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: 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/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 53c1c12b6..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; } } @@ -1565,7 +1588,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 +2506,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/backend/jvm/EmitControlFlow.java b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java index 090f221c9..42b201648 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(); @@ -452,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__")) { @@ -470,111 +485,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); } diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 568edf892..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 = "995455306"; + public static final String gitCommitId = "9cd24576f"; /** * 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; + } } 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;