From 0ee861c9bdb265111721f13c3fbefb6d2b0a65b7 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 08:28:21 +0100 Subject: [PATCH 1/2] Fix stat _ / lstat _ to use cached stat buffer with proper context stat _ and lstat _ now use the cached stat buffer instead of re-statting the last filename. Returns EBADF when no prior stat succeeded, matching Perl behavior. Added STAT_LASTHANDLE/LSTAT_LASTHANDLE bytecode opcodes so the bytecode compiler correctly handles stat _ (previously compiled _ as a bareword string, causing stat on filename "_"). Both JVM and bytecode paths now pass calling context to statLastHandle/lstatLastHandle so scalar context correctly returns empty string on failure. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeInterpreter.java | 8 ++++ .../backend/bytecode/CompileOperator.java | 42 +++++++++++++------ .../bytecode/OpcodeHandlerExtended.java | 14 +++++++ .../perlonjava/backend/bytecode/Opcodes.java | 8 ++++ .../perlonjava/backend/jvm/EmitOperator.java | 16 +++---- .../perlonjava/runtime/operators/Stat.java | 35 ++++++++++++++-- 6 files changed, 96 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 4930bd3c7..ca9c24196 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1244,6 +1244,14 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c pc = OpcodeHandlerExtended.executeLstat(bytecode, pc, registers); break; + case Opcodes.STAT_LASTHANDLE: + pc = OpcodeHandlerExtended.executeStatLastHandle(bytecode, pc, registers); + break; + + case Opcodes.LSTAT_LASTHANDLE: + pc = OpcodeHandlerExtended.executeLstatLastHandle(bytecode, pc, registers); + break; + // File test operations (opcodes 190-216) - delegated to handler case Opcodes.FILETEST_R: case Opcodes.FILETEST_W: diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 4374be4fe..bc387b96a 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -554,21 +554,37 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode } } else if (op.equals("stat") || op.equals("lstat")) { // stat FILE or lstat FILE - int savedContext = bytecodeCompiler.currentCallContext; - bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; - try { - node.operand.accept(bytecodeCompiler); - int operandReg = bytecodeCompiler.lastResultReg; + boolean isUnderscoreOperand = (node.operand instanceof IdentifierNode) + && ((IdentifierNode) node.operand).name.equals("_"); - int rd = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(op.equals("stat") ? Opcodes.STAT : Opcodes.LSTAT); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(operandReg); - bytecodeCompiler.emit(savedContext); // Pass calling context + if (isUnderscoreOperand) { + int savedContext = bytecodeCompiler.currentCallContext; + try { + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(op.equals("stat") ? Opcodes.STAT_LASTHANDLE : Opcodes.LSTAT_LASTHANDLE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emit(savedContext); + bytecodeCompiler.lastResultReg = rd; + } finally { + bytecodeCompiler.currentCallContext = savedContext; + } + } else { + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + try { + node.operand.accept(bytecodeCompiler); + int operandReg = bytecodeCompiler.lastResultReg; - bytecodeCompiler.lastResultReg = rd; - } finally { - bytecodeCompiler.currentCallContext = savedContext; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(op.equals("stat") ? Opcodes.STAT : Opcodes.LSTAT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(operandReg); + bytecodeCompiler.emit(savedContext); + + bytecodeCompiler.lastResultReg = rd; + } finally { + bytecodeCompiler.currentCallContext = savedContext; + } } } else if (op.startsWith("-") && op.length() == 2) { // File test operators: -r, -w, -x, etc. diff --git a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java index 4849678ec..5b543dc6f 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java +++ b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java @@ -513,6 +513,20 @@ public static int executeLstat(int[] bytecode, int pc, RuntimeBase[] registers) return pc; } + public static int executeStatLastHandle(int[] bytecode, int pc, RuntimeBase[] registers) { + int rd = bytecode[pc++]; + int ctx = bytecode[pc++]; + registers[rd] = Stat.statLastHandle(ctx); + return pc; + } + + public static int executeLstatLastHandle(int[] bytecode, int pc, RuntimeBase[] registers) { + int rd = bytecode[pc++]; + int ctx = bytecode[pc++]; + registers[rd] = Stat.lstatLastHandle(ctx); + return pc; + } + /** * Execute print operation. * Format: PRINT contentReg filehandleReg diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 56fa0170b..315919082 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -1149,5 +1149,13 @@ public class Opcodes { /** Defined-or assignment: rd //= rs. Format: DEFINED_OR_ASSIGN rd rs */ public static final short DEFINED_OR_ASSIGN = 349; + /** stat _ (use cached stat buffer): rd = Stat.statLastHandle() + * Format: STAT_LASTHANDLE rd ctx */ + public static final short STAT_LASTHANDLE = 350; + + /** lstat _ (use cached stat buffer): rd = Stat.lstatLastHandle() + * Format: LSTAT_LASTHANDLE rd ctx */ + public static final short LSTAT_LASTHANDLE = 351; + private Opcodes() {} // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java index f8cde803e..c7b436290 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java @@ -864,24 +864,20 @@ static void handleStatOperator(EmitterVisitor emitterVisitor, OperatorNode node, if (node.operand instanceof IdentifierNode identNode && identNode.name.equals("_")) { - // stat _ or lstat _ - still use the old methods since they don't take args + // stat _ or lstat _ - use cached stat buffer with context + emitterVisitor.pushCallContext(); emitterVisitor.ctx.mv.visitMethodInsn( Opcodes.INVOKESTATIC, "org/perlonjava/runtime/operators/Stat", operator + "LastHandle", - "()Lorg/perlonjava/runtime/runtimetypes/RuntimeList;", + "(I)Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;", false); - // Handle context - treat as list that needs conversion if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { handleVoidContext(emitterVisitor); } else if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) { - // Convert with stat's special semantics - emitterVisitor.ctx.mv.visitMethodInsn( - Opcodes.INVOKEVIRTUAL, - "org/perlonjava/runtime/runtimetypes/RuntimeList", - "statScalar", - "()Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", - false); + emitterVisitor.ctx.mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/runtimetypes/RuntimeScalar"); + } else if (emitterVisitor.ctx.contextType == RuntimeContextType.LIST) { + emitterVisitor.ctx.mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/runtimetypes/RuntimeList"); } } else { // stat EXPR or lstat EXPR - use context-aware methods diff --git a/src/main/java/org/perlonjava/runtime/operators/Stat.java b/src/main/java/org/perlonjava/runtime/operators/Stat.java index 77b558879..8dea5f768 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Stat.java +++ b/src/main/java/org/perlonjava/runtime/operators/Stat.java @@ -21,6 +21,7 @@ import static org.perlonjava.runtime.operators.FileTestOperator.lastBasicAttr; import static org.perlonjava.runtime.operators.FileTestOperator.lastFileHandle; import static org.perlonjava.runtime.operators.FileTestOperator.lastPosixAttr; +import static org.perlonjava.runtime.operators.FileTestOperator.lastStatOk; import static org.perlonjava.runtime.operators.FileTestOperator.updateLastStat; import static org.perlonjava.runtime.runtimetypes.GlobalVariable.getGlobalVariable; import static org.perlonjava.runtime.runtimetypes.RuntimeIO.resolvePath; @@ -63,12 +64,38 @@ private static int getPermissionsOctal(BasicFileAttributes basicAttr, PosixFileA return permissions; } - public static RuntimeList statLastHandle() { - return stat(lastFileHandle); + public static RuntimeBase statLastHandle(int ctx) { + if (!lastStatOk) { + getGlobalVariable("main::!").set(9); + RuntimeList empty = new RuntimeList(); + if (ctx == RuntimeContextType.SCALAR) { + return empty.statScalar(); + } + return empty; + } + RuntimeList res = new RuntimeList(); + statInternal(res, lastBasicAttr, lastPosixAttr); + if (ctx == RuntimeContextType.SCALAR) { + return res.statScalar(); + } + return res; } - public static RuntimeList lstatLastHandle() { - return lstat(lastFileHandle); + public static RuntimeBase lstatLastHandle(int ctx) { + if (!lastStatOk) { + getGlobalVariable("main::!").set(9); + RuntimeList empty = new RuntimeList(); + if (ctx == RuntimeContextType.SCALAR) { + return empty.statScalar(); + } + return empty; + } + RuntimeList res = new RuntimeList(); + statInternal(res, lastBasicAttr, lastPosixAttr); + if (ctx == RuntimeContextType.SCALAR) { + return res.statScalar(); + } + return res; } /** From 0934dbb3d620e7c8d2f98266b521be1003dd6beb Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 08:52:07 +0100 Subject: [PATCH 2/2] Fix signatures.t regression and signature error messages Two fixes: 1. Move package inheritance from BytecodeCompiler.compile() to EmitterMethodCreator.compileToInterpreter(). The compile() method is called by all paths including eval STRING (RuntimeCode and EvalStringHandler), which already set the package correctly via setCompilePackage(). The unconditional override in compile() clobbered the correct package with the post-parse symbolTable package, breaking 155 signature tests that use eval(). 2. Fix signature error messages: use "expected N" instead of "expected at most/least N" when min == max parameters. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../org/perlonjava/backend/bytecode/BytecodeCompiler.java | 7 +------ .../org/perlonjava/backend/jvm/EmitterMethodCreator.java | 6 ++++++ .../org/perlonjava/frontend/parser/SignatureParser.java | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index eedb514b2..a1fa58687 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -493,12 +493,7 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { // Use the calling context from EmitterContext for top-level expressions // This is crucial for eval STRING to propagate context correctly currentCallContext = ctx.contextType; - // Inherit package from the JVM compiler context so unqualified sub calls - // resolve in the correct package (not main) - if (ctx.symbolTable != null) { - symbolTable.setCurrentPackage(ctx.symbolTable.getCurrentPackage(), - ctx.symbolTable.currentPackageIsClass()); - } + } // If we have captured variables, allocate registers for them diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java index 6151612e7..c755d7ca9 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -1619,6 +1619,12 @@ private static InterpretedCode compileToInterpreter( ctx.errorUtil ); + // Inherit package from the JVM compiler context so unqualified sub calls + // resolve in the correct package (not main) in the interpreter fallback. + if (ctx.symbolTable != null) { + compiler.setCompilePackage(ctx.symbolTable.getCurrentPackage()); + } + // Compile AST to interpreter bytecode (pass ctx for package context and closure detection) InterpretedCode code = compiler.compile(ast, ctx); diff --git a/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java b/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java index 409b12510..7f88d7934 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java @@ -503,7 +503,7 @@ private Node generateTooFewArgsMessage() { new StringNode("Too few arguments for subroutine '" + fullName + "' (got ", parser.tokenIndex), argCount, parser.tokenIndex), - new StringNode("; expected at least ", parser.tokenIndex), + new StringNode(minParams == maxParams ? "; expected " : "; expected at least ", parser.tokenIndex), parser.tokenIndex), new NumberNode(Integer.toString(adjustedMin), parser.tokenIndex), parser.tokenIndex), @@ -539,7 +539,7 @@ private Node generateTooManyArgsMessage() { new StringNode("Too many arguments for subroutine '" + fullName + "' (got ", parser.tokenIndex), argCount, parser.tokenIndex), - new StringNode("; expected at most ", parser.tokenIndex), + new StringNode(minParams == maxParams ? "; expected " : "; expected at most ", parser.tokenIndex), parser.tokenIndex), new NumberNode(Integer.toString(adjustedMax), parser.tokenIndex), parser.tokenIndex),