Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -4085,8 +4085,18 @@ public void visit(For1Node node) {
emitReg(listReg);

// Step 3: Allocate loop variable register BEFORE entering scope
// This ensures both iterReg and varReg are protected from recycling
int varReg = allocateRegister();
// For pre-existing lexical variables (e.g., `my $k; foreach $k (...)`),
// reuse the existing register so FOREACH_NEXT_OR_EXIT writes to the same
// slot the loop body reads from.
int varReg = -1;
if (globalLoopVarName == null && node.variable instanceof OperatorNode varOp
&& varOp.operator.equals("$") && varOp.operand instanceof IdentifierNode idNode) {
String varName = "$" + idNode.name;
varReg = getVariableRegister(varName);
}
if (varReg == -1) {
varReg = allocateRegister();
}

// Step 3b: For global loop variable: emit LOCAL_SCALAR_SAVE_LEVEL.
// This atomically saves getLocalLevel() into levelReg (pre-push), then calls makeLocal.
Expand All @@ -4106,9 +4116,9 @@ public void visit(For1Node node) {

// Step 5: If we have a named lexical loop variable, add it to the scope now
if (node.variable != null && node.variable instanceof OperatorNode) {
OperatorNode varOp = (OperatorNode) node.variable;
if (varOp.operator.equals("my") && varOp.operand instanceof OperatorNode) {
OperatorNode sigilOp = (OperatorNode) varOp.operand;
OperatorNode varOp2 = (OperatorNode) node.variable;
if (varOp2.operator.equals("my") && varOp2.operand instanceof OperatorNode) {
OperatorNode sigilOp = (OperatorNode) varOp2.operand;
if (sigilOp.operator.equals("$") && sigilOp.operand instanceof IdentifierNode) {
String varName = "$" + ((IdentifierNode) sigilOp.operand).name;
variableScopes.peek().put(varName, varReg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,17 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
break;
}

case Opcodes.CREATE_GOTO: {
int rd = bytecode[pc++];
int labelIdx = bytecode[pc++];
String label = labelIdx == 255 ? null : code.stringPool[labelIdx];
registers[rd] = new RuntimeControlFlowList(
ControlFlowType.GOTO, label,
code.sourceName, code.sourceLine
);
break;
}

case Opcodes.IS_CONTROL_FLOW: {
// Check if value is control flow: rd = (rs instanceof RuntimeControlFlowList)
int rd = bytecode[pc++];
Expand Down Expand Up @@ -1285,6 +1296,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:
Expand Down
51 changes: 40 additions & 11 deletions src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -554,21 +554,31 @@ 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("_");

if (isUnderscoreOperand) {
int rd = bytecodeCompiler.allocateRegister();
bytecodeCompiler.emit(op.equals("stat") ? Opcodes.STAT : Opcodes.LSTAT);
bytecodeCompiler.emit(op.equals("stat") ? Opcodes.STAT_LASTHANDLE : Opcodes.LSTAT_LASTHANDLE);
bytecodeCompiler.emitReg(rd);
bytecodeCompiler.emitReg(operandReg);
bytecodeCompiler.emit(savedContext); // Pass calling context

bytecodeCompiler.emit(bytecodeCompiler.currentCallContext);
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;

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.
Expand Down Expand Up @@ -2895,6 +2905,25 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode
bytecodeCompiler.emitReg(fileReg);
bytecodeCompiler.emit(bytecodeCompiler.currentCallContext);
bytecodeCompiler.lastResultReg = rd;
} else if (op.equals("goto")) {
String labelStr = null;
if (node.operand instanceof ListNode labelNode && !labelNode.elements.isEmpty()) {
Node arg = labelNode.elements.getFirst();
if (arg instanceof IdentifierNode) {
labelStr = ((IdentifierNode) arg).name;
}
}
if (labelStr == null) {
bytecodeCompiler.throwCompilerException("goto must be given label");
}
int rd = bytecodeCompiler.allocateRegister();
bytecodeCompiler.emit(Opcodes.CREATE_GOTO);
bytecodeCompiler.emitReg(rd);
int labelIdx = bytecodeCompiler.addToStringPool(labelStr);
bytecodeCompiler.emitReg(labelIdx);
bytecodeCompiler.emit(Opcodes.RETURN);
bytecodeCompiler.emitReg(rd);
bytecodeCompiler.lastResultReg = -1;
} else {
bytecodeCompiler.throwCompilerException("Unsupported operator: " + op);
}
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,16 @@ public String disassemble() {
int lstatCtx = bytecode[pc++];
sb.append("LSTAT r").append(rd).append(" = lstat(r").append(rs).append(", ctx=").append(lstatCtx).append(")\n");
break;
case Opcodes.STAT_LASTHANDLE:
rd = bytecode[pc++];
int slhCtx = bytecode[pc++];
sb.append("STAT_LASTHANDLE r").append(rd).append(" = stat(_, ctx=").append(slhCtx).append(")\n");
break;
case Opcodes.LSTAT_LASTHANDLE:
rd = bytecode[pc++];
int llhCtx = bytecode[pc++];
sb.append("LSTAT_LASTHANDLE r").append(rd).append(" = lstat(_, ctx=").append(llhCtx).append(")\n");
break;
case Opcodes.FILETEST_R:
rd = bytecode[pc++];
rs = bytecode[pc++];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions src/main/java/org/perlonjava/frontend/parser/OperatorParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,15 @@ static OperatorNode parseStat(Parser parser, LexerToken token, int currentIndex)
paren = true;
}

if (nextToken.text.equals("_")) {
TokenUtils.consume(parser);
if (paren) {
TokenUtils.consume(parser, OPERATOR, ")");
}
return new OperatorNode(token.text,
new IdentifierNode("_", parser.tokenIndex), parser.tokenIndex);
}

// stat/lstat: bareword filehandle (typically ALLCAPS) should be treated as a typeglob.
// Consume it here, before generic expression parsing can turn it into a subroutine call.
if (nextToken.type == IDENTIFIER) {
Expand All @@ -710,15 +719,6 @@ static OperatorNode parseStat(Parser parser, LexerToken token, int currentIndex)
return new OperatorNode(token.text, operand, currentIndex);
}
}
if (nextToken.text.equals("_")) {
// Handle `stat _`
TokenUtils.consume(parser);
if (paren) {
TokenUtils.consume(parser, OPERATOR, ")");
}
return new OperatorNode(token.text,
new IdentifierNode("_", parser.tokenIndex), parser.tokenIndex);
}

// Parse optional single argument (or default to $_)
// If we've already consumed '(', we must parse a full expression up to ')'.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ private static void handleListOrHashArgument(Parser parser, ListNode args, boole
parser.tokenIndex = saveIndex;
}

ListNode argList = ListParser.parseZeroOrMoreList(parser, 0, false, true, false, false);
ListNode argList = ListParser.parseZeroOrMoreList(parser, 0, false, false, false, false);
// @ and % consume remaining arguments in LIST context
// for (Node element : argList.elements) {
// element.setAnnotation("context", "LIST");
Expand Down
13 changes: 8 additions & 5 deletions src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,8 @@ public class CustomFileChannel implements IOHandle {
*/
private final FileChannel fileChannel;

/**
* Tracks whether end-of-file has been reached during reading
*/
private final Path filePath;

private boolean isEOF;

// When true, writes should always occur at end-of-file (Perl's append semantics).
Expand All @@ -82,6 +81,7 @@ public class CustomFileChannel implements IOHandle {
* @throws IOException if an I/O error occurs opening the file
*/
public CustomFileChannel(Path path, Set<StandardOpenOption> options) throws IOException {
this.filePath = path;
this.fileChannel = FileChannel.open(path, options);
this.isEOF = false;
this.appendMode = false;
Expand All @@ -99,11 +99,10 @@ public CustomFileChannel(Path path, Set<StandardOpenOption> options) throws IOEx
* @throws IllegalArgumentException if options don't contain READ or WRITE
*/
public CustomFileChannel(FileDescriptor fd, Set<StandardOpenOption> options) throws IOException {
this.filePath = null;
if (options.contains(StandardOpenOption.READ)) {
// Create a read channel from the file descriptor
this.fileChannel = new FileInputStream(fd).getChannel();
} else if (options.contains(StandardOpenOption.WRITE)) {
// Create a write channel from the file descriptor
this.fileChannel = new FileOutputStream(fd).getChannel();
} else {
throw new IllegalArgumentException("Invalid options for FileDescriptor");
Expand All @@ -112,6 +111,10 @@ public CustomFileChannel(FileDescriptor fd, Set<StandardOpenOption> options) thr
this.appendMode = false;
}

public Path getFilePath() {
return filePath;
}

public void setAppendMode(boolean appendMode) {
this.appendMode = appendMode;
}
Expand Down
28 changes: 10 additions & 18 deletions src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,8 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl
System.err.flush();
}

// Search through the class hierarchy starting from the specified index
// Perl MRO: first pass — search all classes (including UNIVERSAL) for the method.
// AUTOLOAD is only checked after the entire hierarchy has been searched.
for (int i = startFromIndex; i < linearizedClasses.size(); i++) {
String className = linearizedClasses.get(i);
String effectiveClassName = GlobalVariable.resolveStashAlias(className);
Expand All @@ -314,42 +315,33 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl
System.err.flush();
}

// Check if method exists in current class
if (GlobalVariable.existsGlobalCodeRef(normalizedClassMethodName)) {
RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRef(normalizedClassMethodName);
// Perl method lookup should ignore undefined CODE slots (e.g. after `undef *pkg::method`).
if (!codeRef.getDefinedBoolean()) {
continue;
}
// Cache the found method
cacheMethod(cacheKey, codeRef);

if (TRACE_METHOD_RESOLUTION) {
System.err.println(" FOUND method!");
System.err.flush();
}

return codeRef;
}
}

// Method not found in current class, check AUTOLOAD
if (!autoloadEnabled || methodName.startsWith("(")) {
// refuse to AUTOLOAD tie() flags and overload markers (all start with "(")
} else {
// Check for AUTOLOAD in current class
// Second pass — method not found anywhere, check AUTOLOAD in class hierarchy.
// This matches Perl semantics: AUTOLOAD is only tried after the full MRO
// search (including UNIVERSAL) fails to find the method.
if (autoloadEnabled && !methodName.startsWith("(")) {
for (int i = startFromIndex; i < linearizedClasses.size(); i++) {
String className = linearizedClasses.get(i);
String effectiveClassName = GlobalVariable.resolveStashAlias(className);
String autoloadName = (effectiveClassName.endsWith("::") ? effectiveClassName : effectiveClassName + "::") + "AUTOLOAD";
if (GlobalVariable.existsGlobalCodeRef(autoloadName)) {
RuntimeScalar autoload = GlobalVariable.getGlobalCodeRef(autoloadName);
if (autoload.getDefinedBoolean()) {
// System.out.println("AUTOLOAD: " + autoloadName + " looking for " + methodName);

// The caller will need to set $AUTOLOAD before calling
((RuntimeCode) autoload.value).autoloadVariableName = autoloadName;

// Cache the found method;
// In case AUTOLOAD creates the missing method, it will invalidate the cache
cacheMethod(cacheKey, autoload);

return autoload;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package org.perlonjava.runtime.operators;

import org.perlonjava.runtime.io.ClosedIOHandle;
import org.perlonjava.runtime.io.CustomFileChannel;
import org.perlonjava.runtime.io.IOHandle;
import org.perlonjava.runtime.io.LayeredIOHandle;
import org.perlonjava.runtime.runtimetypes.RuntimeGlob;
import org.perlonjava.runtime.runtimetypes.PerlCompilerException;
import org.perlonjava.runtime.runtimetypes.RuntimeCode;
Expand Down Expand Up @@ -259,7 +262,18 @@ public static RuntimeScalar fileTest(String operator, RuntimeScalar fileHandle)
return scalarUndef;
}

// For file test operators on file handles, return undef and set EBADF
// Try to get the file path from the handle for stat-based file tests
IOHandle innerHandle = fh.ioHandle;
while (innerHandle instanceof LayeredIOHandle lh) {
innerHandle = lh.getDelegate();
}
if (innerHandle instanceof CustomFileChannel cfc) {
Path path = cfc.getFilePath();
if (path != null) {
return fileTest(operator, new RuntimeScalar(path.toString()));
}
}
// Fallback for non-file handles (pipes, sockets, etc.)
getGlobalVariable("main::!").set(9);
updateLastStat(fileHandle, false, 9);
return scalarUndef;
Expand Down
Loading