Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions src/main/java/org/perlonjava/codegen/EmitOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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",
Expand Down
16 changes: 12 additions & 4 deletions src/main/java/org/perlonjava/operators/FileTestOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/perlonjava/operators/OperatorHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;");
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/org/perlonjava/operators/Stat.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;


Expand Down Expand Up @@ -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);
Expand Down
12 changes: 11 additions & 1 deletion src/main/java/org/perlonjava/runtime/RuntimeGlob.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/perlonjava/runtime/RuntimeIO.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/org/perlonjava/runtime/RuntimeList.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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.
Expand Down
16 changes: 14 additions & 2 deletions src/main/java/org/perlonjava/runtime/RuntimeScalar.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
};
}

Expand Down