diff --git a/src/main/java/org/perlonjava/codegen/EmitOperator.java b/src/main/java/org/perlonjava/codegen/EmitOperator.java index 5bede9ba2..acd466e16 100644 --- a/src/main/java/org/perlonjava/codegen/EmitOperator.java +++ b/src/main/java/org/perlonjava/codegen/EmitOperator.java @@ -581,17 +581,56 @@ static void handleDoFileOperator(EmitterVisitor emitterVisitor, OperatorNode nod } static void handleStatOperator(EmitterVisitor emitterVisitor, OperatorNode node, String operator) { + // stat/lstat have special scalar context behavior: + // - Empty list (failure) -> "" (empty string) + // - Non-empty list (success) -> 1 (true) + if (node.operand instanceof IdentifierNode identNode && identNode.name.equals("_")) { + // stat _ or lstat _ - still use the old methods since they don't take args emitterVisitor.ctx.mv.visitMethodInsn( Opcodes.INVOKESTATIC, "org/perlonjava/operators/Stat", operator + "LastHandle", "()Lorg/perlonjava/runtime/RuntimeList;", false); - handleVoidContext(emitterVisitor); + // 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/RuntimeList", + "statScalar", + "()Lorg/perlonjava/runtime/RuntimeScalar;", + false); + } } else { - handleUnaryDefaultCase(node, operator, emitterVisitor); + // stat EXPR or lstat EXPR - use context-aware methods + node.operand.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); + emitterVisitor.pushCallContext(); // Push context onto stack + emitterVisitor.ctx.mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + "org/perlonjava/operators/Stat", + operator, + "(Lorg/perlonjava/runtime/RuntimeScalar;I)Lorg/perlonjava/runtime/RuntimeBase;", + false); + + // Cast to the appropriate type for the bytecode verifier + if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) { + // In scalar context, stat returns RuntimeScalar + emitterVisitor.ctx.mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/RuntimeScalar"); + } else if (emitterVisitor.ctx.contextType == RuntimeContextType.LIST) { + // In list context, stat returns RuntimeList + emitterVisitor.ctx.mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/RuntimeList"); + } + // In RUNTIME or VOID context, leave as RuntimeBase (no cast needed) + + // Handle void context + if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { + handleVoidContext(emitterVisitor); + } } } diff --git a/src/main/java/org/perlonjava/codegen/EmitOperatorFileTest.java b/src/main/java/org/perlonjava/codegen/EmitOperatorFileTest.java index 6e849b8c9..c7f662a87 100644 --- a/src/main/java/org/perlonjava/codegen/EmitOperatorFileTest.java +++ b/src/main/java/org/perlonjava/codegen/EmitOperatorFileTest.java @@ -32,7 +32,11 @@ static void handleFileTestBuiltin(EmitterVisitor emitterVisitor, OperatorNode no fileOperand = currentNode; break; } + } else if (opNode.operand instanceof OperatorNode) { + // Continue traversing if the operand is another filetest operator + currentNode = opNode.operand; } else { + // Found the file operand fileOperand = opNode.operand; break; } @@ -52,7 +56,7 @@ static void handleFileTestBuiltin(EmitterVisitor emitterVisitor, OperatorNode no emitterVisitor.ctx.mv.visitInsn(Opcodes.AASTORE); } - if (fileOperand instanceof IdentifierNode && ((IdentifierNode) fileOperand).name.equals("_")) { + if (fileOperand == null || (fileOperand instanceof IdentifierNode && ((IdentifierNode) fileOperand).name.equals("_"))) { emitterVisitor.ctx.mv.visitMethodInsn( Opcodes.INVOKESTATIC, "org/perlonjava/operators/FileTestOperator", diff --git a/src/main/java/org/perlonjava/operators/FileTestOperator.java b/src/main/java/org/perlonjava/operators/FileTestOperator.java index 59e08f9ba..f32057041 100644 --- a/src/main/java/org/perlonjava/operators/FileTestOperator.java +++ b/src/main/java/org/perlonjava/operators/FileTestOperator.java @@ -380,11 +380,19 @@ public static RuntimeScalar fileTest(String operator, RuntimeScalar fileHandle) } public static RuntimeScalar chainedFileTest(String[] operators, RuntimeScalar fileHandle) { - RuntimeScalar currentHandle = fileHandle; - for (String operator : operators) { - currentHandle = fileTest(operator, currentHandle); + // Execute operators from right to left + // First operator uses the provided fileHandle, subsequent ones use lastFileHandle (_) + RuntimeScalar result = null; + for (int i = 0; i < operators.length; i++) { + if (i == 0) { + // First operator (rightmost in the source) uses the provided fileHandle + result = fileTest(operators[i], fileHandle); + } else { + // Subsequent operators use lastFileHandle (_) + result = fileTest(operators[i], lastFileHandle); + } } - return currentHandle; + return result; } public static RuntimeScalar chainedFileTestLastHandle(String[] operators) { diff --git a/src/main/java/org/perlonjava/operators/OperatorHandler.java b/src/main/java/org/perlonjava/operators/OperatorHandler.java index 79d1b8d97..012194a86 100644 --- a/src/main/java/org/perlonjava/operators/OperatorHandler.java +++ b/src/main/java/org/perlonjava/operators/OperatorHandler.java @@ -219,8 +219,8 @@ public record OperatorHandler(String className, String methodName, int methodTyp put("reverse", "reverse", "org/perlonjava/operators/Operator", "(I[Lorg/perlonjava/runtime/RuntimeBase;)Lorg/perlonjava/runtime/RuntimeBase;"); put("crypt", "crypt", "org/perlonjava/operators/Crypt", "(Lorg/perlonjava/runtime/RuntimeList;)Lorg/perlonjava/runtime/RuntimeScalar;"); put("unlink", "unlink", "org/perlonjava/operators/UnlinkOperator", "(I[Lorg/perlonjava/runtime/RuntimeBase;)Lorg/perlonjava/runtime/RuntimeBase;"); - put("stat", "stat", "org/perlonjava/operators/Stat", "(Lorg/perlonjava/runtime/RuntimeScalar;)Lorg/perlonjava/runtime/RuntimeList;"); - put("lstat", "lstat", "org/perlonjava/operators/Stat", "(Lorg/perlonjava/runtime/RuntimeScalar;)Lorg/perlonjava/runtime/RuntimeList;"); + put("stat", "stat", "org/perlonjava/operators/Stat", "(Lorg/perlonjava/runtime/RuntimeScalar;I)Lorg/perlonjava/runtime/RuntimeBase;"); + put("lstat", "lstat", "org/perlonjava/operators/Stat", "(Lorg/perlonjava/runtime/RuntimeScalar;I)Lorg/perlonjava/runtime/RuntimeBase;"); put("vec", "vec", "org/perlonjava/operators/Vec", "(Lorg/perlonjava/runtime/RuntimeList;)Lorg/perlonjava/runtime/RuntimeScalar;"); put("chmod", "chmod", "org/perlonjava/operators/Operator", "(Lorg/perlonjava/runtime/RuntimeList;)Lorg/perlonjava/runtime/RuntimeScalar;"); put("link", "link", "org/perlonjava/nativ/NativeUtils", "(I[Lorg/perlonjava/runtime/RuntimeBase;)Lorg/perlonjava/runtime/RuntimeScalar;"); diff --git a/src/main/java/org/perlonjava/operators/Stat.java b/src/main/java/org/perlonjava/operators/Stat.java index 37aef2696..dcc9146bd 100644 --- a/src/main/java/org/perlonjava/operators/Stat.java +++ b/src/main/java/org/perlonjava/operators/Stat.java @@ -1,6 +1,8 @@ package org.perlonjava.operators; import org.perlonjava.io.ClosedIOHandle; +import org.perlonjava.runtime.RuntimeBase; +import org.perlonjava.runtime.RuntimeContextType; import org.perlonjava.runtime.RuntimeIO; import org.perlonjava.runtime.RuntimeList; import org.perlonjava.runtime.RuntimeScalar; @@ -20,6 +22,7 @@ import static org.perlonjava.runtime.GlobalVariable.getGlobalVariable; import static org.perlonjava.runtime.RuntimeIO.resolvePath; import static org.perlonjava.runtime.RuntimeScalarCache.getScalarInt; +import static org.perlonjava.runtime.RuntimeScalarCache.scalarTrue; import static org.perlonjava.runtime.RuntimeScalarCache.scalarUndef; @@ -65,6 +68,36 @@ public static RuntimeList statLastHandle() { public static RuntimeList lstatLastHandle() { return lstat(lastFileHandle); } + + /** + * stat with context awareness + * @param arg the file or filehandle to stat + * @param ctx the calling context (SCALAR, LIST, VOID, or RUNTIME) + * @return RuntimeScalar in scalar context, RuntimeList otherwise + */ + public static RuntimeBase stat(RuntimeScalar arg, int ctx) { + RuntimeList result = stat(arg); + if (ctx == RuntimeContextType.SCALAR) { + // stat in scalar context: empty list -> "", non-empty list -> 1 + return result.isEmpty() ? new RuntimeScalar("") : scalarTrue; + } + return result; + } + + /** + * lstat with context awareness + * @param arg the file or filehandle to lstat + * @param ctx the calling context (SCALAR, LIST, VOID, or RUNTIME) + * @return RuntimeScalar in scalar context, RuntimeList otherwise + */ + public static RuntimeBase lstat(RuntimeScalar arg, int ctx) { + RuntimeList result = lstat(arg); + if (ctx == RuntimeContextType.SCALAR) { + // lstat in scalar context: empty list -> "", non-empty list -> 1 + return result.isEmpty() ? new RuntimeScalar("") : scalarTrue; + } + return result; + } public static RuntimeList stat(RuntimeScalar arg) { lastFileHandle.set(arg); diff --git a/src/main/java/org/perlonjava/runtime/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/RuntimeGlob.java index 0ec77078c..c735113e6 100644 --- a/src/main/java/org/perlonjava/runtime/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/RuntimeGlob.java @@ -209,7 +209,17 @@ private RuntimeScalar getGlobSlot(RuntimeScalar index) { } yield new RuntimeScalar(); // Return undef if code doesn't exist } - case "IO" -> IO; + case "IO" -> { + // In Perl, accessing the IO slot returns a GLOB reference that can be blessed + // Convert GLOB type to GLOBREFERENCE so it behaves like other references + if (IO.type == RuntimeScalarType.GLOB && IO.value instanceof RuntimeIO) { + RuntimeScalar ioRef = new RuntimeScalar(); + ioRef.type = RuntimeScalarType.GLOBREFERENCE; + ioRef.value = IO.value; + yield ioRef; + } + yield IO; + } case "SCALAR" -> GlobalVariable.getGlobalVariable(this.globName); case "ARRAY" -> { // Only return reference if array exists (has elements or was explicitly created) diff --git a/src/main/java/org/perlonjava/runtime/RuntimeIO.java b/src/main/java/org/perlonjava/runtime/RuntimeIO.java index a4890fe19..f719564df 100644 --- a/src/main/java/org/perlonjava/runtime/RuntimeIO.java +++ b/src/main/java/org/perlonjava/runtime/RuntimeIO.java @@ -58,7 +58,7 @@ Handling pipes (e.g., |- or -| modes). * @see DirectoryIO * @see RuntimeScalarReference */ -public class RuntimeIO implements RuntimeScalarReference { +public class RuntimeIO extends RuntimeScalar { /** * Mapping of Perl file modes to their corresponding Java NIO StandardOpenOption sets. diff --git a/src/main/java/org/perlonjava/runtime/RuntimeList.java b/src/main/java/org/perlonjava/runtime/RuntimeList.java index 8e81b27cb..eb81e0b33 100644 --- a/src/main/java/org/perlonjava/runtime/RuntimeList.java +++ b/src/main/java/org/perlonjava/runtime/RuntimeList.java @@ -6,6 +6,7 @@ import java.util.Map; import java.util.NoSuchElementException; +import static org.perlonjava.runtime.RuntimeScalarCache.scalarTrue; import static org.perlonjava.runtime.RuntimeScalarCache.scalarUndef; /** @@ -323,6 +324,19 @@ public RuntimeScalar scalar() { return elements.getLast().scalar(); } + /** + * Special scalar conversion for stat/lstat operators. + * In Perl, stat in scalar context returns: + * - "" (empty string) if stat fails (empty list) + * - 1 (true) if stat succeeds (non-empty list) + */ + public RuntimeScalar statScalar() { + if (isEmpty()) { + return new RuntimeScalar(""); // Empty string, not undef + } + return scalarTrue; // Return 1 for success + } + /** * Creates a reference from a list. * For single-element lists (e.g., from constant subs), creates a reference to that element. diff --git a/src/main/java/org/perlonjava/runtime/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/RuntimeScalar.java index 1710b5f4f..cb7301150 100644 --- a/src/main/java/org/perlonjava/runtime/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/RuntimeScalar.java @@ -714,7 +714,7 @@ public String toStringRef() { if (value == null) { yield "GLOB(0x" + scalarUndef.hashCode() + ")"; } - yield ((RuntimeGlob) value).toStringRef(); + yield ((RuntimeBase) value).toStringRef(); } case REFERENCE -> { // Determine the proper type name for the reference @@ -853,6 +853,10 @@ public RuntimeArray arrayDeref() { RuntimeGlob glob = (RuntimeGlob) value; yield GlobalVariable.getGlobalArray(glob.globName); } + case GLOBREFERENCE -> { + // GLOBREFERENCE (like *STDOUT{IO}) is not an array reference + throw new PerlCompilerException("Not an ARRAY reference"); + } case STRING, BYTE_STRING -> throw new PerlCompilerException("Can't use string (\"" + this + "\") as an ARRAY ref while \"strict refs\" in use"); case TIED_SCALAR -> tiedFetch().arrayDeref(); @@ -918,6 +922,10 @@ public RuntimeHash hashDeref() { RuntimeGlob glob = (RuntimeGlob) value; yield GlobalVariable.getGlobalHash(glob.globName); } + case GLOBREFERENCE -> { + // GLOBREFERENCE (like *STDOUT{IO}) is not a hash reference + throw new PerlCompilerException("Not a HASH reference"); + } case STRING, BYTE_STRING -> // Strict refs violation: attempting to use a string as a hash ref throw new PerlCompilerException("Can't use string (\"" + this + "\") as a HASH ref while \"strict refs\" in use"); @@ -951,7 +959,11 @@ public RuntimeScalar scalarDeref() { case STRING, BYTE_STRING -> throw new PerlCompilerException("Can't use string (\"" + this + "\") as a SCALAR ref while \"strict refs\" in use"); case TIED_SCALAR -> tiedFetch().scalarDeref(); - default -> throw new PerlCompilerException("Variable does not contain a scalar reference"); + case GLOBREFERENCE -> { + // GLOBREFERENCE (like *STDOUT{IO}) is not a scalar reference + throw new PerlCompilerException("Not a SCALAR reference"); + } + default -> throw new PerlCompilerException("Not a SCALAR reference"); }; }