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
361 changes: 361 additions & 0 deletions dev/modules/app_perlbrew.md

Large diffs are not rendered by default.

29 changes: 28 additions & 1 deletion src/main/java/org/perlonjava/app/cli/ArgumentParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ private static void processArgs(String[] args, CompilerOptions parsedArgs) {

// Iterate over each argument
for (int i = 0; i < args.length; i++) {
if (readingArgv || !args[i].startsWith("-")) {
if (readingArgv || !args[i].startsWith("-") || args[i].equals("-")) {
// Process non-switch arguments (e.g., file names or positional arguments)
processNonSwitchArgument(args, parsedArgs, i);
readingArgv = true; // Once a non-switch argument is encountered, treat all subsequent arguments as such
Expand Down Expand Up @@ -224,6 +224,33 @@ private static void processNonSwitchArgument(String[] args, CompilerOptions pars
if (parsedArgs.code == null) {
// If no code has been set, treat the argument as a file name
parsedArgs.fileName = args[index];

// Handle "-" as "read from stdin" (standard Perl behavior: perl - arg1 arg2)
if (parsedArgs.fileName.equals("-")) {
// When -M modules are present (e.g., system($^X, '-MModule', '-', args)),
// the modules typically call exit() during import before stdin code runs.
// Use empty code to avoid blocking on stdin in this common pattern.
if (!parsedArgs.moduleUseStatements.isEmpty()) {
parsedArgs.code = "";
return;
}
// No -M modules: read code from stdin
try {
StringBuilder stdinContent = new StringBuilder();
java.io.BufferedReader reader = new java.io.BufferedReader(
new java.io.InputStreamReader(System.in));
String line;
while ((line = reader.readLine()) != null) {
stdinContent.append(line).append("\n");
}
parsedArgs.code = stdinContent.toString();
} catch (IOException e) {
System.err.println("Error: Unable to read from stdin");
System.exit(1);
}
return;
}

try {
String filePath = parsedArgs.fileName;
if (parsedArgs.usePathEnv && !filePath.contains("/") && !filePath.contains("\\")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,15 @@ public static RuntimeList executePerlAST(Node ast,
private static RuntimeList executeCode(RuntimeCode runtimeCode, EmitterContext ctx, boolean isMainProgram, int callerContext) throws Exception {
runUnitcheckBlocks(ctx.unitcheckBlocks);
if (isMainProgram) {
runCheckBlocks();
// Push a CallerStack entry so caller() inside CHECK/INIT/END blocks
// sees the main program as their caller, matching Perl 5 behavior
// where these blocks run from the main program scope.
CallerStack.push("main", ctx.compilerOptions.fileName, 0);
try {
runCheckBlocks();
} finally {
CallerStack.pop();
}
}
if (ctx.compilerOptions.compileOnly) {
RuntimeIO.closeAllHandles();
Expand All @@ -345,7 +353,12 @@ private static RuntimeList executeCode(RuntimeCode runtimeCode, EmitterContext c
RuntimeList result;
try {
if (isMainProgram) {
runInitBlocks();
CallerStack.push("main", ctx.compilerOptions.fileName, 0);
try {
runInitBlocks();
} finally {
CallerStack.pop();
}
}

// Use the caller's context if specified, otherwise use default behavior
Expand All @@ -357,7 +370,12 @@ private static RuntimeList executeCode(RuntimeCode runtimeCode, EmitterContext c

try {
if (isMainProgram) {
runEndBlocks();
CallerStack.push("main", ctx.compilerOptions.fileName, 0);
try {
runEndBlocks();
} finally {
CallerStack.pop();
}
}
} catch (Throwable endException) {
RuntimeIO.closeAllHandles();
Expand All @@ -369,9 +387,19 @@ private static RuntimeList executeCode(RuntimeCode runtimeCode, EmitterContext c
// PerlExitException already ran END blocks and closed handles in WarnDie.exit()
// Just re-throw for the caller to handle
throw e;
} catch (PerlNonLocalReturnException e) {
// A non-local return escaped to the top level (e.g., return inside map/grep
// at the top level). In Perl 5, this produces "Can't return outside a subroutine".
// Consume the exception and treat the return value as the program result.
result = e.returnValue != null ? e.returnValue.getList() : new RuntimeList();
} catch (Throwable t) {
if (isMainProgram) {
runEndBlocks(false); // Don't reset $? on exception path
CallerStack.push("main", ctx.compilerOptions.fileName, 0);
try {
runEndBlocks(false); // Don't reset $? on exception path
} finally {
CallerStack.pop();
}
RuntimeIO.closeAllHandles();
}
if (t instanceof RuntimeException runtimeException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ public class BytecodeCompiler implements Visitor {
private boolean isEvalString;
// True when compiling inside a defer block (control flow out of defer is prohibited)
private boolean isInDeferBlock;
// True when compiling inside a map/grep block (explicit return must use RETURN_NONLOCAL)
boolean isInMapGrepBlock;
// Nesting depth inside eval blocks (goto &sub from eval is prohibited)
// 0 = not in eval block, >0 = inside eval block(s)
int evalBlockDepth;
Expand Down Expand Up @@ -4951,6 +4953,12 @@ private void visitAnonymousSubroutine(SubroutineNode node) {
subCompiler.isInDeferBlock = true;
}

// Check if this subroutine is a map/grep block - explicit return must use RETURN_NONLOCAL
Boolean isMapGrepBlock = (Boolean) node.getAnnotation("isMapGrepBlock");
if (isMapGrepBlock != null && isMapGrepBlock) {
subCompiler.isInMapGrepBlock = true;
}

// Subroutine bodies should use RUNTIME context so the calling context
// (VOID/SCALAR/LIST) propagates correctly at runtime via register 2 (wantarray).
// Without this, the default LIST context is baked into all opcodes,
Expand All @@ -4964,6 +4972,12 @@ private void visitAnonymousSubroutine(SubroutineNode node) {
subCode.attributes = node.attributes;
subCode.packageName = getCurrentPackage();

// Copy the isMapGrepBlock flag to the runtime code object so that
// ListOperators.map/grep and RuntimeCode.apply() can detect non-local returns
if (subCompiler.isInMapGrepBlock) {
subCode.isMapGrepBlock = true;
}

if (RuntimeCode.DISASSEMBLE) {
System.out.println(Disassemble.disassemble(subCode));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,29 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
RuntimeBase retVal = registers[retReg];

if (retVal == null) {
return new RuntimeList();
retVal = new RuntimeList();
}
RuntimeList retList = retVal.getList();
RuntimeCode.materializeSpecialVarsInResult(retList);

return retList;
}

case Opcodes.RETURN_NONLOCAL -> {
// Non-local return from map/grep block (explicit 'return' statement):
// wrap in RETURN marker so it propagates to enclosing subroutine
int retReg = bytecode[pc++];
RuntimeBase retVal = registers[retReg];

if (retVal == null) {
retVal = new RuntimeList();
}
RuntimeList retList = retVal.getList();
RuntimeCode.materializeSpecialVarsInResult(retList);

return new RuntimeControlFlowList(retList, code.sourceName, code.sourceLine);
}

case Opcodes.GOTO -> {
// Unconditional jump: pc = offset
int offset = readInt(bytecode, pc);
Expand Down Expand Up @@ -961,6 +977,17 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
// Check for control flow (last/next/redo/goto) - TAILCALL already handled above
if (result.isNonLocalGoto()) {
RuntimeControlFlowList flow = (RuntimeControlFlowList) result;

// Handle RETURN markers: consume at non-map/grep boundaries, propagate in map/grep
if (flow.getControlFlowType() == ControlFlowType.RETURN) {
if (!code.isMapGrepBlock) {
// Consume: unwrap and return the value from this subroutine
RuntimeBase retVal = flow.getReturnValue();
return retVal != null ? retVal.getList() : new RuntimeList();
}
return result; // Propagate in map/grep blocks
}

// Check labeled block stack for a matching label
boolean handled = false;
for (int i = labeledBlockStack.size() - 1; i >= 0; i--) {
Expand Down Expand Up @@ -1065,6 +1092,16 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
// Check for control flow (last/next/redo/goto) - TAILCALL already handled above
if (result.isNonLocalGoto()) {
RuntimeControlFlowList flow = (RuntimeControlFlowList) result;

// Handle RETURN markers: consume at non-map/grep boundaries, propagate in map/grep
if (flow.getControlFlowType() == ControlFlowType.RETURN) {
if (!code.isMapGrepBlock) {
RuntimeBase retVal = flow.getReturnValue();
return retVal != null ? retVal.getList() : new RuntimeList();
}
return result;
}

boolean handled = false;
for (int i = labeledBlockStack.size() - 1; i >= 0; i--) {
int[] entry = labeledBlockStack.get(i);
Expand Down Expand Up @@ -2469,8 +2506,7 @@ private static int executeScopeOps(int opcode, int[] bytecode, int pc,
int nameIdx = bytecode[pc++];
String fullName = code.stringPool[nameIdx];

RuntimeArray arr = GlobalVariable.getGlobalArray(fullName);
DynamicVariableManager.pushLocalVariable(arr);
GlobalRuntimeArray.makeLocal(fullName);
registers[rd] = GlobalVariable.getGlobalArray(fullName);
return pc;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,21 +65,17 @@ private static boolean handleLocalAssignment(BytecodeCompiler bc, BinaryOperator
bc.lastResultReg = regIdx;
return true;
}
bc.emit(Opcodes.LOAD_GLOBAL_ARRAY);
bc.emitWithToken(Opcodes.LOCAL_ARRAY, node.getIndex());
bc.emitReg(localReg);
bc.emit(nameIdx);
bc.emit(Opcodes.PUSH_LOCAL_VARIABLE);
bc.emitReg(localReg);
bc.emit(Opcodes.ARRAY_SET_FROM_LIST);
bc.emitReg(localReg);
bc.emitReg(valueReg);
}
case "%" -> {
bc.emit(Opcodes.LOAD_GLOBAL_HASH);
bc.emitWithToken(Opcodes.LOCAL_HASH, node.getIndex());
bc.emitReg(localReg);
bc.emit(nameIdx);
bc.emit(Opcodes.PUSH_LOCAL_VARIABLE);
bc.emitReg(localReg);
bc.emit(Opcodes.HASH_SET_FROM_LIST);
bc.emitReg(localReg);
bc.emitReg(valueReg);
Expand Down Expand Up @@ -830,19 +826,24 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler,
bytecodeCompiler.emit(nameIdx);
}

// In scalar context, count RHS elements BEFORE hash assignment
// (the assignment may modify the RHS if it's the same hash)
int countReg = -1;
if (outerContext == RuntimeContextType.SCALAR) {
countReg = bytecodeCompiler.allocateRegister();
bytecodeCompiler.emit(Opcodes.LIST_TO_COUNT);
bytecodeCompiler.emitReg(countReg);
bytecodeCompiler.emitReg(valueReg);
}

// Populate hash from list using setFromList
bytecodeCompiler.emit(Opcodes.HASH_SET_FROM_LIST);
bytecodeCompiler.emitReg(hashReg);
bytecodeCompiler.emitReg(valueReg);

// In scalar context, return the hash size; in list context, return the hash
// Return the pre-computed count or the hash
if (outerContext == RuntimeContextType.SCALAR) {
// Convert hash to scalar (returns bucket info like "3/8")
int sizeReg = bytecodeCompiler.allocateRegister();
bytecodeCompiler.emit(Opcodes.ARRAY_SIZE);
bytecodeCompiler.emitReg(sizeReg);
bytecodeCompiler.emitReg(hashReg);
bytecodeCompiler.lastResultReg = sizeReg;
bytecodeCompiler.lastResultReg = countReg;
} else {
bytecodeCompiler.lastResultReg = hashReg;
}
Expand Down Expand Up @@ -1659,30 +1660,33 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler,
bytecodeCompiler.emitReg(rhsListReg);
bytecodeCompiler.emitReg(rhsReg);

int countReg = -1;
if (outerContext == RuntimeContextType.SCALAR) {
countReg = bytecodeCompiler.allocateRegister();
bytecodeCompiler.emit(Opcodes.ARRAY_SIZE);
bytecodeCompiler.emitReg(countReg);
bytecodeCompiler.emitReg(rhsListReg);
}

// Compile LHS ListNode in LIST context - this produces a RuntimeList of lvalues
// This follows the JVM backend approach (EmitVariable.java line 837)
bytecodeCompiler.compileNode(listNode, -1, RuntimeContextType.LIST);
int lhsListReg = bytecodeCompiler.lastResultReg;

// Call SET_FROM_LIST to assign RHS values to LHS lvalues
// setFromList() returns a RuntimeArray with scalarContextSize set to the
// original RHS element count, and elements containing the assigned values.
// Note: rhsListReg is consumed (elements cleared) by setFromList's addToArray.
int resultReg = bytecodeCompiler.allocateRegister();
bytecodeCompiler.emit(Opcodes.SET_FROM_LIST);
bytecodeCompiler.emitReg(resultReg);
bytecodeCompiler.emitReg(lhsListReg);
bytecodeCompiler.emitReg(rhsListReg);

if (countReg >= 0) {
if (outerContext == RuntimeContextType.SCALAR) {
// In scalar context, return the RHS element count.
// resultReg is a RuntimeArray with scalarContextSize set;
// ARRAY_SIZE on it calls scalar() which returns scalarContextSize.
int countReg = bytecodeCompiler.allocateRegister();
bytecodeCompiler.emit(Opcodes.ARRAY_SIZE);
bytecodeCompiler.emitReg(countReg);
bytecodeCompiler.emitReg(resultReg);
bytecodeCompiler.lastResultReg = countReg;
} else {
bytecodeCompiler.lastResultReg = rhsListReg;
// In list context, return the assigned values (after hash dedup etc.)
bytecodeCompiler.lastResultReg = resultReg;
}

} else {
Expand Down
Loading
Loading