From 1992b28f9a6711a6d6b6c5020a8093a75f046768 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 31 Mar 2026 16:59:59 +0200 Subject: [PATCH 1/6] Fix interpreter state variable persistence and dynamic goto EXPR fallback - BytecodeCompiler: Split BEGIN-captured variables (RETRIEVE_BEGIN_*) from state variables without initializers (STATE_INIT_*). State vars previously used RETRIEVE_BEGIN_SCALAR which calls removeGlobalVariable(), destroying state on each call. Now uses STATE_INIT_SCALAR which persists correctly. - BytecodeCompiler: Add LoopInfo push/pop for bare blocks so redo/next/last can find their target loop in the interpreter. - EmitControlFlow: Dynamic goto EXPR (computed label) now triggers interpreter fallback instead of creating a GOTO marker that silently exits. The interpreter supports GOTO_DYNAMIC with label-to-PC lookup. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../backend/bytecode/BytecodeCompiler.java | 94 ++++++++++++++- .../backend/jvm/EmitControlFlow.java | 108 +----------------- 2 files changed, 98 insertions(+), 104 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 27c290458..1b7c55680 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -2261,8 +2261,9 @@ void compileVariableDeclaration(OperatorNode node, String op) { Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); Integer beginId = RuntimeCode.evalBeginIds.get(sigilOp); - if (beginId != null || op.equals("state")) { - int persistId = beginId != null ? beginId : sigilOp.id; + if (beginId != null) { + // BEGIN-captured variable: use RETRIEVE_BEGIN_* (destructive removal from global storage) + int persistId = beginId; int reg = allocateRegister(); int nameIdx = addToStringPool(varName); @@ -2303,6 +2304,62 @@ void compileVariableDeclaration(OperatorNode node, String op) { } return; } + if (op.equals("state")) { + // State variable without initializer: use STATE_INIT_* (non-destructive) + // This preserves the variable across subroutine calls + int persistId = sigilOp.id; + int reg = allocateRegister(); + int nameIdx = addToStringPool(varName); + + // Allocate a register for the undef/empty initial value + int undefReg = allocateRegister(); + + switch (sigil) { + case "$" -> { + emit(Opcodes.LOAD_UNDEF); + emitReg(undefReg); + emitWithToken(Opcodes.STATE_INIT_SCALAR, node.getIndex()); + emitReg(reg); + emitReg(undefReg); + emit(nameIdx); + emit(persistId); + registerVariable(varName, reg); + } + case "@" -> { + emit(Opcodes.NEW_ARRAY); + emitReg(undefReg); + emitWithToken(Opcodes.STATE_INIT_ARRAY, node.getIndex()); + emitReg(reg); + emitReg(undefReg); + emit(nameIdx); + emit(persistId); + registerVariable(varName, reg); + } + case "%" -> { + emit(Opcodes.NEW_HASH); + emitReg(undefReg); + emitWithToken(Opcodes.STATE_INIT_HASH, node.getIndex()); + emitReg(reg); + emitReg(undefReg); + emit(nameIdx); + emit(persistId); + registerVariable(varName, reg); + } + default -> throwCompilerException("Unsupported variable type: " + sigil); + } + + // If this is a declared reference, create a reference to it + if (isDeclaredReference && currentCallContext != RuntimeContextType.VOID) { + int refReg = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg); + emitReg(reg); + lastResultReg = refReg; + } else { + lastResultReg = reg; + } + return; + } // Regular lexical variable (not captured, not state) int reg = addVariable(varName, "my"); @@ -4963,6 +5020,17 @@ public void visit(For3Node node) { emitInt(0); } + // Push loop info so that redo/next/last inside bare blocks work + // (Perl 5 allows redo/next/last in bare blocks) + // Unlabeled bare blocks are targets for unlabeled redo/next/last; + // labeled blocks (like SKIP: { }) are only targeted by matching label. + int bodyStartPc = bytecode.size(); + boolean isUnlabeledTarget = (node.labelName == null); + LoopInfo loopInfo = new LoopInfo( + isUnlabeledTarget ? null : node.labelName, + bodyStartPc, true); + loopStack.push(loopInfo); + enterScope(); try { if (node.body != null) { @@ -4976,12 +5044,34 @@ public void visit(For3Node node) { exitScope(); } + // next jumps here (continue point = end of body, before exit) + loopInfo.continuePc = bytecode.size(); + + // Pop loop info and patch jump targets + loopStack.pop(); + // Patch redo PCs to jump to body start + for (int pc2 : loopInfo.redoPcs) { + patchJump(pc2, bodyStartPc); + } + // Patch next PCs to jump to continue point + for (int pc2 : loopInfo.nextPcs) { + patchJump(pc2, loopInfo.continuePc); + } + // Patch last (break) PCs - will be patched to end label below + // (we need the end PC first) + if (node.labelName != null) { emit(Opcodes.POP_LABELED_BLOCK); int exitPc = bytecode.size(); patchJump(exitPcPlaceholder, exitPc); } + // Patch last (break) PCs to jump past the block + int endPc = bytecode.size(); + for (int pc2 : loopInfo.breakPcs) { + patchJump(pc2, endPc); + } + lastResultReg = outerResultReg; return; } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java index 090f221c9..3a00c746a 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java @@ -402,7 +402,6 @@ static void handleGotoLabel(EmitterVisitor emitterVisitor, OperatorNode node) { // Parse the goto argument String labelName = null; - boolean isDynamic = false; if (node.operand instanceof ListNode labelNode && !labelNode.elements.isEmpty()) { Node arg = labelNode.elements.getFirst(); @@ -470,111 +469,16 @@ static void handleGotoLabel(EmitterVisitor emitterVisitor, OperatorNode node) { } // Dynamic label (goto EXPR) - expression evaluated at runtime - isDynamic = true; - if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("visit(goto): Dynamic goto with expression"); - - // 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/runtimetypes/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. - // Teardown (defer blocks, local variables) happens at returnLabel - // before this marker is returned to the caller. - ctx.mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/runtimetypes/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/runtimetypes/RuntimeControlFlowList", - "", - "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Lorg/perlonjava/runtime/runtimetypes/RuntimeArray;Ljava/lang/String;I)V", - false); - - 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/runtimetypes/RuntimeScalar", - "toString", - "()Ljava/lang/String;", - false); - - // 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/runtimetypes/RuntimeControlFlowList"); - ctx.mv.visitInsn(Opcodes.DUP_X1); - ctx.mv.visitInsn(Opcodes.SWAP); - - ctx.mv.visitFieldInsn(Opcodes.GETSTATIC, - "org/perlonjava/runtime/runtimetypes/ControlFlowType", - "GOTO", - "Lorg/perlonjava/runtime/runtimetypes/ControlFlowType;"); - ctx.mv.visitInsn(Opcodes.SWAP); - - // 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/runtimetypes/RuntimeControlFlowList", - "", - "(Lorg/perlonjava/runtime/runtimetypes/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) { - markerSlot = ctx.symbolTable.allocateLocalVariable(); - } - ctx.mv.visitVarInsn(Opcodes.ASTORE, markerSlot); - - // Jump to returnLabel with the marker - ctx.mv.visitVarInsn(Opcodes.ALOAD, markerSlot); - if (pooledMarker) { - ctx.javaClassInfo.releaseSpillSlot(); - } - ctx.mv.visitVarInsn(Opcodes.ASTORE, ctx.javaClassInfo.returnValueSlot); - ctx.mv.visitJumpInsn(Opcodes.GOTO, ctx.javaClassInfo.returnLabel); - return; + // JVM backend can't dispatch dynamic goto labels (the GOTO marker propagates + // out of all scopes and silently exits). Fall back to interpreter which + // supports GOTO_DYNAMIC with label-to-PC lookup. + throw new PerlCompilerException(node.tokenIndex, + "Dynamic goto EXPR requires interpreter fallback", ctx.errorUtil); } } // Ensure label is provided for static goto - if (labelName == null && !isDynamic) { + if (labelName == null) { throw new PerlCompilerException(node.tokenIndex, "goto must be given label", ctx.errorUtil); } From 06254d1b84a99280046e0b24e971387d550e4f7d Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 31 Mar 2026 17:05:11 +0200 Subject: [PATCH 2/6] Add compile-time error for state variables in list assignment Perl 5 forbids initializing state variables in list context, e.g.: (state $a) = 1; state ($a, $b) = (); (state $a, my $b) = (); Added validation in ParseInfix.java that detects state declarations on the left side of list assignments and throws: "Initialization of state variables in list currently forbidden" This fixes 46 tests in op/state.t (tests 108-153), bringing the pass rate from 95/170 to 141/170. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org/perlonjava/core/Configuration.java | 2 +- .../frontend/parser/ParseInfix.java | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 568edf892..da1a04400 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "995455306"; + public static final String gitCommitId = "2374d61f6"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/frontend/parser/ParseInfix.java b/src/main/java/org/perlonjava/frontend/parser/ParseInfix.java index 9713382e7..a4531de69 100644 --- a/src/main/java/org/perlonjava/frontend/parser/ParseInfix.java +++ b/src/main/java/org/perlonjava/frontend/parser/ParseInfix.java @@ -135,6 +135,11 @@ public static Node parseInfixOperation(Parser parser, Node left, int precedence) // Validate operator chaining rules (Perl 5.32+) validateOperatorChaining(parser, operator, left, right); + // Validate that state variables are not initialized in list context + if (operator.equals("=")) { + validateNoStateInListAssignment(parser, left); + } + BinaryOperatorNode node = new BinaryOperatorNode(operator, left, right, parser.tokenIndex); // Annotate arithmetic nodes with 'use integer' state so constant folding @@ -524,4 +529,50 @@ private static void validateOperatorChaining(Parser parser, String operator, Nod } } } + + /** + * Validates that state variables are not used in list assignment context. + * In Perl 5, constructs like (state $a) = 1 or state ($a, $b) = () are forbidden + * with the error "Initialization of state variables in list currently forbidden". + * + * @param parser The parser instance + * @param left The left-hand side of the assignment + */ + private static void validateNoStateInListAssignment(Parser parser, Node left) { + // Case 1: state ($a) = 1 or state ($a, $b) = () + // Left side is OperatorNode("state") with a ListNode operand + if (left instanceof OperatorNode opNode && opNode.operator.equals("state") + && opNode.operand instanceof ListNode) { + throw new PerlCompilerException( + parser.tokenIndex, + "Initialization of state variables in list currently forbidden", + parser.ctx.errorUtil); + } + + // Case 2: (state $a) = 1 or (state $a, state $b) = () or (state $a, $b) = () + // Left side is a ListNode that contains state declarations + if (left instanceof ListNode listNode && containsStateDeclaration(listNode)) { + throw new PerlCompilerException( + parser.tokenIndex, + "Initialization of state variables in list currently forbidden", + parser.ctx.errorUtil); + } + } + + /** + * Checks if a ListNode contains any state variable declarations, + * either directly or nested within parenthesized sub-lists. + */ + private static boolean containsStateDeclaration(ListNode listNode) { + for (Node element : listNode.elements) { + if (element instanceof OperatorNode opNode && opNode.operator.equals("state")) { + return true; + } + // Check nested lists: (state ($a)) = 1 + if (element instanceof ListNode nestedList && containsStateDeclaration(nestedList)) { + return true; + } + } + return false; + } } From 9a8c6aabd7d4e99dc3eb1a0eb68b3e5cf8093427 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 31 Mar 2026 17:11:03 +0200 Subject: [PATCH 3/6] Update test improvement plan with op/state.t progress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record PR #414 results: op/state.t 69/170 → 141/170. Mark item 1.5 as done, update Category 5 table, add progress tracking section. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- dev/design/test_pass_rate_improvement_plan.md | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/dev/design/test_pass_rate_improvement_plan.md b/dev/design/test_pass_rate_improvement_plan.md index c46585e7a..00e5cc812 100644 --- a/dev/design/test_pass_rate_improvement_plan.md +++ b/dev/design/test_pass_rate_improvement_plan.md @@ -144,7 +144,7 @@ These are tests that start running but crash fatally, blocking all subsequent te | Test File | Passed/Total | Blocking Bug | Tests Unblocked | |-----------|-------------|--------------|-----------------| | op/ref.t | 96/265 | `@UNIVERSAL::ISA` not followed in MRO | **156** | -| op/state.t | 69/170 | Dynamic `goto $variable` silently exits | **101** | +| op/state.t | 141/170 | Dynamic `goto $variable` → interpreter fallback + state fixes | **15+14** | | op/heredoc.t | 66/138 | Heredoc inside `eval 's//<<~/e'` | **~60** | | op/filetest.t | 227/436 | NPE from NUL in filename (`-T "TEST\0"`) | **5** | | op/universal.t | 90/142 | `$1` undef after successful regex match | **38** | @@ -252,7 +252,7 @@ These are small, targeted fixes that each unblock many tests: | 1.2 | Null-check in `fileno()` for unopened handles | 8 (fh.t) | Tiny | | 1.3 | Null-guard in file test operator for NUL-in-filename | 5 (filetest.t) | Tiny | | 1.4 | `@UNIVERSAL::ISA` traversal in MRO | 156 (ref.t) | Small | -| 1.5 | Fix dynamic `goto $variable` | 101 (state.t) | Small | +| 1.5 | Fix dynamic `goto $variable` + state var persistence | ~~101~~ **done** (72 gained) | Small | | 1.6 | Extend `vec()` to 64-bit widths | 28 (vec.t) | Small | | 1.7 | Fix `$1` capture after successful match (state corruption) | 38 (universal.t) | Small | | 1.8 | Fix unrecognized-switch error message (add trailing `.`) | ~56 (switches.t) | Tiny | @@ -326,7 +326,42 @@ These tests are inherently incompatible with PerlOnJava and should not be target --- -## Tracking +## Progress Tracking + +### Current Status: Phase 1 in progress + +### Completed Fixes + +#### PR #414: op/state.t — 69/170 → 141/170 (2025-03-31) + +Branch: `fix/state-attribute-validation` + +| Fix | Tests Gained | Category | +|-----|-------------|----------| +| Interpreter: state variable persistence (`STATE_INIT_*` instead of `RETRIEVE_BEGIN_*`) | ~26 | 1.5 | +| Interpreter: bare block `redo` (add `LoopInfo` push/pop) | (enabler) | 1.5 | +| JVM backend: dynamic `goto EXPR` triggers interpreter fallback | (enabler) | 1.5 | +| Parser: "state in list context forbidden" compile error | +46 | New | + +Files changed: +- `src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java` — state var compilation fix + bare block redo +- `src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java` — dynamic goto EXPR fallback +- `src/main/java/org/perlonjava/frontend/parser/ParseInfix.java` — state-in-list validation + +Remaining op/state.t failures (15 + 14 blocked): +- Tests 42-43: `:shared` attribute silently accepted (pre-existing) +- Tests 62-68: goto + state interaction (pass standalone, fail in full test context) +- Tests 94, 96, 98, 100: `given`/`when` (incomplete support) +- Tests 103, 106-107: state in closures/anon subs (state sharing between copies) +- Tests 157-170: blocked (execution stops at test 156) + +### Next Steps + +1. Continue Phase 1 quick wins (items 1.1-1.10) +2. Investigate op/state.t goto+state interaction failures in full test context +3. Run full test suite to measure overall progress + +### Baseline Update this document as fixes land. Use the test runner to measure progress: From b6ae7f0db1b6f528ee5ea713256fe33697d77623 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 31 Mar 2026 17:49:47 +0200 Subject: [PATCH 4/6] Add missing interpreter opcodes and fix lazy CORE:: subroutine generation Interpreter backend: - Add opcodes for symlink, chroot, mkdir, exec, fcntl, ioctl, getpwent, setpwent, endpwent, msgctl, shmctl, semctl - Wire existing IPC opcodes (shmget, shmread, shmwrite, semget, semop, msgget, msgsnd, msgrcv) into CompileOperator - Add NUL-byte guard for file test operators - Add chroot and setpriority stubs This fixes "Unsupported operator" errors when large test files (like op/taint.t) trigger interpreter fallback. CORE:: subroutine wrappers: - Add lazy generation in all RuntimeCode.apply() overloads - &CORE::reverse("hello") and similar calls now work correctly - Previously only \&CORE::reverse worked; direct &CORE::X calls threw "Undefined subroutine" because the wrapper was never generated Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../backend/bytecode/BytecodeInterpreter.java | 42 +++++++++++++++++- .../backend/bytecode/CompileOperator.java | 20 +++++++++ .../backend/bytecode/Disassemble.java | 26 ++++++++++- .../backend/bytecode/MiscOpcodeHandler.java | 12 ++++++ .../perlonjava/backend/bytecode/Opcodes.java | 14 ++++++ .../org/perlonjava/core/Configuration.java | 2 +- .../runtime/operators/FileTestOperator.java | 7 +++ .../runtime/operators/Operator.java | 7 +++ .../runtime/operators/OperatorHandler.java | 2 + .../runtime/operators/SystemOperator.java | 9 ++++ .../runtime/runtimetypes/RuntimeCode.java | 43 +++++++++++++++++++ 11 files changed, 181 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 53c1c12b6..f1e21988c 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1565,7 +1565,11 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.CHOWN, Opcodes.WAITPID, Opcodes.FORK, Opcodes.GETPPID, Opcodes.GETPGRP, Opcodes.SETPGRP, Opcodes.GETPRIORITY, Opcodes.SETPRIORITY, Opcodes.GETSOCKOPT, Opcodes.SETSOCKOPT, Opcodes.SYSCALL, Opcodes.SEMGET, Opcodes.SEMOP, Opcodes.MSGGET, - Opcodes.MSGSND, Opcodes.MSGRCV, Opcodes.SHMGET, Opcodes.SHMREAD, Opcodes.SHMWRITE -> { + Opcodes.MSGSND, Opcodes.MSGRCV, Opcodes.SHMGET, Opcodes.SHMREAD, Opcodes.SHMWRITE, + Opcodes.SYMLINK, Opcodes.CHROOT, Opcodes.MKDIR, + Opcodes.MSGCTL, Opcodes.SHMCTL, Opcodes.SEMCTL, + Opcodes.EXEC, Opcodes.FCNTL, Opcodes.IOCTL, + Opcodes.GETPWENT, Opcodes.SETPWENT, Opcodes.ENDPWENT -> { pc = executeSystemOps(opcode, bytecode, pc, registers); } @@ -2479,6 +2483,42 @@ private static int executeSystemOps(int opcode, int[] bytecode, int pc, case Opcodes.SHMWRITE -> { return SlowOpcodeHandler.executeShmwrite(bytecode, pc, registers); } + case Opcodes.SYMLINK -> { + return MiscOpcodeHandler.execute(Opcodes.SYMLINK, bytecode, pc, registers); + } + case Opcodes.CHROOT -> { + return MiscOpcodeHandler.execute(Opcodes.CHROOT, bytecode, pc, registers); + } + case Opcodes.MKDIR -> { + return MiscOpcodeHandler.execute(Opcodes.MKDIR, bytecode, pc, registers); + } + case Opcodes.MSGCTL -> { + return MiscOpcodeHandler.execute(Opcodes.MSGCTL, bytecode, pc, registers); + } + case Opcodes.SHMCTL -> { + return MiscOpcodeHandler.execute(Opcodes.SHMCTL, bytecode, pc, registers); + } + case Opcodes.SEMCTL -> { + return MiscOpcodeHandler.execute(Opcodes.SEMCTL, bytecode, pc, registers); + } + case Opcodes.EXEC -> { + return MiscOpcodeHandler.execute(Opcodes.EXEC, bytecode, pc, registers); + } + case Opcodes.FCNTL -> { + return MiscOpcodeHandler.execute(Opcodes.FCNTL, bytecode, pc, registers); + } + case Opcodes.IOCTL -> { + return MiscOpcodeHandler.execute(Opcodes.IOCTL, bytecode, pc, registers); + } + case Opcodes.GETPWENT -> { + return MiscOpcodeHandler.execute(Opcodes.GETPWENT, bytecode, pc, registers); + } + case Opcodes.SETPWENT -> { + return MiscOpcodeHandler.execute(Opcodes.SETPWENT, bytecode, pc, registers); + } + case Opcodes.ENDPWENT -> { + return MiscOpcodeHandler.execute(Opcodes.ENDPWENT, bytecode, pc, registers); + } default -> throw new RuntimeException("Unknown system opcode: " + opcode); } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index e325f7e5c..7e43c3b24 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -725,6 +725,26 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode case "setpgrp" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SETPGRP); case "getpriority" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETPRIORITY); case "setpriority" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SETPRIORITY); + case "symlink" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SYMLINK); + case "chroot" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.CHROOT); + case "mkdir" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.MKDIR); + case "semctl" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SEMCTL); + case "semget" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SEMGET); + case "semop" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SEMOP); + case "msgget" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.MSGGET); + case "msgsnd" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.MSGSND); + case "msgrcv" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.MSGRCV); + case "msgctl" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.MSGCTL); + case "shmget" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SHMGET); + case "shmread" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SHMREAD); + case "shmwrite" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SHMWRITE); + case "shmctl" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SHMCTL); + case "exec" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.EXEC); + case "fcntl" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.FCNTL); + case "ioctl" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.IOCTL); + case "getpwent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETPWENT); + case "setpwent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SETPWENT); + case "endpwent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.ENDPWENT); case "opendir" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.OPENDIR); case "readdir" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.READDIR); case "seekdir" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SEEKDIR); diff --git a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java index e39de5e8e..f35d12c38 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java @@ -1823,7 +1823,19 @@ public static String disassemble(InterpretedCode interpretedCode) { case Opcodes.GETPRIORITY: case Opcodes.SETPRIORITY: case Opcodes.GETSOCKOPT: - case Opcodes.SETSOCKOPT: { + case Opcodes.SETSOCKOPT: + case Opcodes.SYMLINK: + case Opcodes.CHROOT: + case Opcodes.MKDIR: + case Opcodes.MSGCTL: + case Opcodes.SHMCTL: + case Opcodes.SEMCTL: + case Opcodes.EXEC: + case Opcodes.FCNTL: + case Opcodes.IOCTL: + case Opcodes.GETPWENT: + case Opcodes.SETPWENT: + case Opcodes.ENDPWENT: { rd = interpretedCode.bytecode[pc++]; int sysArgsReg = interpretedCode.bytecode[pc++]; int sysCtx = interpretedCode.bytecode[pc++]; @@ -1836,6 +1848,18 @@ public static String disassemble(InterpretedCode interpretedCode) { case Opcodes.SETPRIORITY -> "setpriority"; case Opcodes.GETSOCKOPT -> "getsockopt"; case Opcodes.SETSOCKOPT -> "setsockopt"; + case Opcodes.SYMLINK -> "symlink"; + case Opcodes.CHROOT -> "chroot"; + case Opcodes.MKDIR -> "mkdir"; + case Opcodes.MSGCTL -> "msgctl"; + case Opcodes.SHMCTL -> "shmctl"; + case Opcodes.SEMCTL -> "semctl"; + case Opcodes.EXEC -> "exec"; + case Opcodes.FCNTL -> "fcntl"; + case Opcodes.IOCTL -> "ioctl"; + case Opcodes.GETPWENT -> "getpwent"; + case Opcodes.SETPWENT -> "setpwent"; + case Opcodes.ENDPWENT -> "endpwent"; default -> "sys_op_" + opcode; }; sb.append(sysName).append(" r").append(rd) diff --git a/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java index 1e5d6a8aa..839f036ba 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java @@ -82,6 +82,18 @@ public static int execute(int opcode, int[] bytecode, int pc, RuntimeBase[] regi case Opcodes.SETPGRP -> Operator.setpgrp(ctx, argsArray); case Opcodes.GETPRIORITY -> Operator.getpriority(ctx, argsArray); case Opcodes.SETPRIORITY -> new RuntimeScalar(0); // stub - no native impl yet + case Opcodes.SYMLINK -> org.perlonjava.runtime.nativ.NativeUtils.symlink(ctx, argsArray); + case Opcodes.CHROOT -> SystemOperator.chroot(ctx, argsArray); + case Opcodes.MKDIR -> Directory.mkdir(args); + case Opcodes.MSGCTL -> new RuntimeScalar(0); // stub + case Opcodes.SHMCTL -> new RuntimeScalar(0); // stub + case Opcodes.SEMCTL -> new RuntimeScalar(0); // stub + case Opcodes.EXEC -> SystemOperator.exec(args, false, ctx); + case Opcodes.FCNTL -> IOOperator.fcntl(ctx, argsArray); + case Opcodes.IOCTL -> IOOperator.ioctl(ctx, argsArray); + case Opcodes.GETPWENT -> ExtendedNativeUtils.getpwent(ctx, argsArray); + case Opcodes.SETPWENT -> ExtendedNativeUtils.setpwent(ctx, argsArray); + case Opcodes.ENDPWENT -> ExtendedNativeUtils.endpwent(ctx, argsArray); case Opcodes.OPENDIR -> Directory.opendir(args); case Opcodes.READDIR -> Directory.readdir(args.elements.isEmpty() ? null : (RuntimeScalar) args.elements.get(0), ctx); diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 250460d9d..c25af477b 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -2040,6 +2040,20 @@ public class Opcodes { // Effect: rd = CompareOperators.smartmatch(rs1, rs2) public static final short SMARTMATCH = 400; + // Missing system operators needed for interpreter fallback of large files (e.g. taint.t) + public static final short SYMLINK = 401; + public static final short CHROOT = 402; + public static final short MKDIR = 403; + public static final short MSGCTL = 404; + public static final short SHMCTL = 405; + public static final short SEMCTL = 406; + public static final short EXEC = 407; + public static final short FCNTL = 408; + public static final short IOCTL = 409; + public static final short GETPWENT = 410; + public static final short SETPWENT = 411; + public static final short ENDPWENT = 412; + private Opcodes() { } // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index da1a04400..160df591c 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "2374d61f6"; + public static final String gitCommitId = "9cad94cea"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java index 2d2f60ddf..1f862fa81 100644 --- a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java @@ -323,6 +323,13 @@ public static RuntimeScalar fileTest(String operator, RuntimeScalar fileHandle) return operator.equals("-l") ? scalarFalse : scalarUndef; } + // Handle NUL bytes in filename - Perl warns and treats as non-existent + if (filename.indexOf('\0') >= 0) { + getGlobalVariable("main::!").set(2); // ENOENT + updateLastStat(fileHandle, false, 2); + return operator.equals("-l") ? scalarFalse : scalarUndef; + } + // Check if it looks like a filehandle name but isn't actually a filehandle if (looksLikeFilehandle(filename)) { // Try to get it as a global variable (filehandle) diff --git a/src/main/java/org/perlonjava/runtime/operators/Operator.java b/src/main/java/org/perlonjava/runtime/operators/Operator.java index c8610205e..0c8875dfc 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Operator.java +++ b/src/main/java/org/perlonjava/runtime/operators/Operator.java @@ -684,6 +684,13 @@ public static RuntimeScalar getpriority(int ctx, RuntimeBase... args) { return new RuntimeScalar(0); } + public static RuntimeScalar setpriority(int ctx, RuntimeBase... args) { + // setpriority(WHICH, WHO, PRIORITY) - set process priority + // Not available on the JVM; return false and set $! + GlobalVariable.setGlobalVariable("main::!", "setpriority() not supported on this platform (Java/JVM)"); + return RuntimeScalarCache.scalarUndef; + } + public static RuntimeList reset(RuntimeList args, int ctx) { if (args.isEmpty()) { RuntimeRegex.reset(); diff --git a/src/main/java/org/perlonjava/runtime/operators/OperatorHandler.java b/src/main/java/org/perlonjava/runtime/operators/OperatorHandler.java index 70a146ed0..bdf06c3dd 100644 --- a/src/main/java/org/perlonjava/runtime/operators/OperatorHandler.java +++ b/src/main/java/org/perlonjava/runtime/operators/OperatorHandler.java @@ -273,6 +273,7 @@ public record OperatorHandler(String className, String methodName, int methodTyp put("system", "system", "org/perlonjava/runtime/operators/SystemOperator", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;ZI)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); put("exec", "exec", "org/perlonjava/runtime/operators/SystemOperator", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;ZI)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); put("fork", "fork", "org/perlonjava/runtime/operators/SystemOperator", "(I[Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); + put("chroot", "chroot", "org/perlonjava/runtime/operators/SystemOperator", "(I[Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); put("kill", "kill", "org/perlonjava/runtime/operators/KillOperator", "(I[Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); put("umask", "umask", "org/perlonjava/runtime/operators/UmaskOperator", "(I[Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); put("wait", "waitForChild", "org/perlonjava/runtime/operators/WaitpidOperator", "()Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); @@ -297,6 +298,7 @@ public record OperatorHandler(String className, String methodName, int methodTyp put("getpgrp", "getpgrp", "org/perlonjava/runtime/operators/Operator", "(I[Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); put("setpgrp", "setpgrp", "org/perlonjava/runtime/operators/Operator", "(I[Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); put("getpriority", "getpriority", "org/perlonjava/runtime/operators/Operator", "(I[Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); + put("setpriority", "setpriority", "org/perlonjava/runtime/operators/Operator", "(I[Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); put("prototype", "prototype", "org/perlonjava/runtime/runtimetypes/RuntimeCode", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Ljava/lang/String;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); diff --git a/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java b/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java index 4e00eac7c..8585c9e8a 100644 --- a/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java @@ -706,6 +706,15 @@ public static RuntimeScalar fork(int ctx, RuntimeBase... args) { // Return undef to indicate failure return scalarUndef; } + + /** + * Stub for chroot() - not supported on the JVM. + * Sets $! and returns undef (false) to indicate failure. + */ + public static RuntimeScalar chroot(int ctx, RuntimeBase... args) { + setGlobalVariable("main::!", "chroot() not supported on this platform (Java/JVM)"); + return scalarUndef; + } /** * Copies the Perl %ENV hash to the ProcessBuilder environment. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 324b89531..798a34339 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -24,6 +24,7 @@ import org.perlonjava.runtime.debugger.DebugState; import org.perlonjava.runtime.operators.ModuleOperators; import org.perlonjava.runtime.operators.WarnDie; +import org.perlonjava.runtime.CoreSubroutineGenerator; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -1859,6 +1860,20 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int } // Check if it's an unfilled forward declaration (not defined) + if (!code.defined()) { + // Lazily generate CORE:: subroutine wrappers on first call + if ("CORE".equals(code.packageName) && code.subName != null) { + boolean generated = CoreSubroutineGenerator.generateWrapper(code.subName); + if (generated) { + // Reload code after wrapper generation + runtimeScalar = GlobalVariable.getGlobalCodeRef("CORE::" + code.subName); + code = (RuntimeCode) runtimeScalar.value; + if (code.defined()) { + // Fall through to normal execution below + } + } + } + } if (!code.defined()) { // Try to find AUTOLOAD for this subroutine String subroutineName = code.packageName + "::" + code.subName; @@ -2046,6 +2061,14 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa code = (RuntimeCode) runtimeScalar.value; } + // Lazily generate CORE:: subroutine wrappers on first call + if (!code.defined() && "CORE".equals(code.packageName) && code.subName != null) { + if (CoreSubroutineGenerator.generateWrapper(code.subName)) { + runtimeScalar = GlobalVariable.getGlobalCodeRef("CORE::" + code.subName); + code = (RuntimeCode) runtimeScalar.value; + } + } + if (code.defined()) { // Look up warning bits for the code's class and push to context stack String warningBits = getWarningBitsForCode(code); @@ -2147,6 +2170,14 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa code = (RuntimeCode) runtimeScalar.value; } + // Lazily generate CORE:: subroutine wrappers on first call + if (!code.defined() && "CORE".equals(code.packageName) && code.subName != null) { + if (CoreSubroutineGenerator.generateWrapper(code.subName)) { + runtimeScalar = GlobalVariable.getGlobalCodeRef("CORE::" + code.subName); + code = (RuntimeCode) runtimeScalar.value; + } + } + if (code.defined()) { // Look up warning bits for the code's class and push to context stack String warningBits = getWarningBitsForCode(code); @@ -2464,6 +2495,18 @@ public RuntimeList apply(RuntimeArray a, int callContext) { // Check if subroutine is defined (prefer functional interface over methodHandle) if (this.subroutine == null && this.methodHandle == null) { + // Lazily generate CORE:: subroutine wrappers on first call + if ("CORE".equals(this.packageName) && this.subName != null) { + if (CoreSubroutineGenerator.generateWrapper(this.subName)) { + RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRef("CORE::" + this.subName); + if (codeRef.type == RuntimeScalarType.CODE) { + RuntimeCode generated = (RuntimeCode) codeRef.value; + if (generated.defined()) { + return generated.apply(a, callContext); + } + } + } + } String fullSubName = ""; if (this.packageName != null && this.subName != null) { fullSubName = this.packageName + "::" + this.subName; From d9255ff3a2491486887a484539acb276b7cc2991 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 31 Mar 2026 21:21:05 +0200 Subject: [PATCH 5/6] Fix GOTO/TAILCALL marker propagation in interpreter eval handling When a GOTO or TAILCALL control flow marker was not handled by labeled blocks in the interpreter CALL_SUB/CALL_METHOD handlers, it would propagate all the way out and cause silent process exit. This happened when code like eval { goto nonexistent_label } ran in functions that triggered interpreter fallback. Now, unhandled GOTO/TAILCALL markers check the evalCatchStack. If an eval catch handler exists, the error variable is set to the error message and execution jumps to the catch handler, matching the JVM backend behavior. Results: goto-sub.t improved from 17/44 to 32/44 (master baseline: 27/44) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../backend/bytecode/BytecodeInterpreter.java | 23 +++++++++++++++++++ .../org/perlonjava/core/Configuration.java | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index f1e21988c..4f7023766 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -948,6 +948,19 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } } if (!handled) { + // GOTO/TAILCALL markers inside eval should be caught + // (same as JVM backend's EmitEval: ordinal > 2 means not LAST/NEXT/REDO) + ControlFlowType cfType = flow.getControlFlowType(); + if ((cfType == ControlFlowType.GOTO || cfType == ControlFlowType.TAILCALL) + && !evalCatchStack.isEmpty()) { + // Set $@ to the error message + String errorMsg = flow.marker.buildErrorMessage(); + GlobalVariable.setGlobalVariable("main::@", errorMsg); + // Jump to eval catch handler + pc = evalCatchStack.pop(); + RuntimeCode.evalDepth--; + break; + } return result; } } @@ -1032,6 +1045,16 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } } if (!handled) { + // GOTO/TAILCALL markers inside eval should be caught + ControlFlowType cfType = flow.getControlFlowType(); + if ((cfType == ControlFlowType.GOTO || cfType == ControlFlowType.TAILCALL) + && !evalCatchStack.isEmpty()) { + String errorMsg = flow.marker.buildErrorMessage(); + GlobalVariable.setGlobalVariable("main::@", errorMsg); + pc = evalCatchStack.pop(); + RuntimeCode.evalDepth--; + break; + } return result; } } diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 160df591c..32e993bff 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "9cad94cea"; + public static final String gitCommitId = "665448fcf"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). From c91566698a768a1dbaae029d926518cafe1c9a6e Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 31 Mar 2026 21:52:37 +0200 Subject: [PATCH 6/6] Handle goto \&NAME in JVM backend to avoid interpreter fallback The JVM backend did not recognize goto \&NAME (backslash-reference to a subroutine) as a tail-call form. It only handled goto &NAME (via BinaryOperatorNode) and goto &{$var}/goto &$sub forms. The \&NAME pattern fell through to the Dynamic goto EXPR catch-all, forcing interpreter fallback. This caused two problems: 1. Defer blocks did not fire before the tail-call (defer.t test 15) 2. Lexical closure variables were not properly shared after fallback The fix recognizes OperatorNode("\\") containing a subroutine reference and routes it through handleGotoSubroutine, which jumps to returnLabel where try-finally cleanup (including defer blocks) runs before the TAILCALL marker is returned to the caller. Results: defer.t restored to 24/33 (master baseline), goto-sub.t 32/44 Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../perlonjava/backend/jvm/EmitControlFlow.java | 16 ++++++++++++++++ .../java/org/perlonjava/core/Configuration.java | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java index 3a00c746a..42b201648 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java @@ -451,6 +451,22 @@ static void handleGotoLabel(EmitterVisitor emitterVisitor, OperatorNode node) { } } + // Check if this is goto \&NAME - backslash-reference to a subroutine + // The parser produces: OperatorNode("\", OperatorNode("&", ...)) + // Perl semantics: goto \&sub passes current @_ to the target + if (arg instanceof OperatorNode refOp && refOp.operator.equals("\\")) { + String evalScope4 = null; + if (ctx.javaClassInfo.isInEvalBlock) evalScope4 = "eval-block"; + else if (ctx.javaClassInfo.isInEvalString) evalScope4 = "eval-string"; + if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("visit(goto): Detected goto \\&NAME tail call"); + ListNode argsNode = new ListNode(node.tokenIndex); + OperatorNode atUnderscore = new OperatorNode("@", + new IdentifierNode("_", node.tokenIndex), node.tokenIndex); + argsNode.elements.add(atUnderscore); + handleGotoSubroutine(emitterVisitor, refOp, argsNode, node.tokenIndex, evalScope4); + return; + } + // Check if this is a tail call (goto EXPR where EXPR is a coderef) // This handles: goto __SUB__, goto $coderef, etc. if (arg instanceof OperatorNode opNode && opNode.operator.equals("__SUB__")) { diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 32e993bff..7f337cb9f 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "665448fcf"; + public static final String gitCommitId = "9cd24576f"; /** * Git commit date of the build (ISO format: YYYY-MM-DD).