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
416 changes: 351 additions & 65 deletions dev/modules/app_perlbrew.md

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion src/main/java/org/perlonjava/app/cli/ArgumentParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,10 @@ private static void processNonSwitchArgument(String[] args, CompilerOptions pars
String filePath = parsedArgs.fileName;
if (parsedArgs.usePathEnv && !filePath.contains("/") && !filePath.contains("\\")) {
// Search in PATH when -S is used and filename has no path separators
String pathEnv = System.getenv("PATH");
// Read from Perl's %ENV first, fall back to Java env
RuntimeHash perlEnv = GlobalVariable.getGlobalHash("main::ENV");
RuntimeScalar pathVal = perlEnv.get(new RuntimeScalar("PATH"));
String pathEnv = pathVal.getDefinedBoolean() ? pathVal.toString() : System.getenv("PATH");
if (pathEnv != null) {
for (String path : pathEnv.split(File.pathSeparator)) {
File file = new File(path, filePath);
Expand Down
18 changes: 12 additions & 6 deletions src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -1290,13 +1290,19 @@ private static void visitLength(BytecodeCompiler bc, OperatorNode node) {
}

private static void visitDiamond(BytecodeCompiler bc, OperatorNode node) {
// Defensive: ensure operand is a ListNode with a StringNode element
String argument = "";
if (node.operand instanceof ListNode listNode && !listNode.elements.isEmpty()
&& listNode.elements.getFirst() instanceof StringNode stringNode) {
argument = stringNode.value;
// Determine whether this is readline (<> or <<>>) or glob (<*.t>, <$var/*.t>).
// After interpolation, glob patterns may produce non-StringNode operands
// (e.g., BinaryOperatorNode for concatenation like $var . "/*.t").
boolean isReadline = false;
if (node.operand instanceof ListNode listNode) {
if (listNode.elements.isEmpty()) {
isReadline = true;
} else if (listNode.elements.getFirst() instanceof StringNode stringNode) {
isReadline = stringNode.value.isEmpty() || stringNode.value.equals("<>");
}
// If element is not a StringNode, it's an interpolated glob pattern → NOT readline
}
if (argument.isEmpty() || argument.equals("<>")) {
if (isReadline) {
bc.compileNode(node.operand, -1, RuntimeContextType.SCALAR);
int fhReg = bc.lastResultReg;
int rd = bc.allocateOutputRegister();
Expand Down
22 changes: 14 additions & 8 deletions src/main/java/org/perlonjava/backend/jvm/EmitOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -609,14 +609,20 @@ static void handleMapOperator(EmitterVisitor emitterVisitor, BinaryOperatorNode
// Handles the 'diamond' operator, which reads input from a file or standard input.
static void handleDiamondBuiltin(EmitterVisitor emitterVisitor, OperatorNode node) {
MethodVisitor mv = emitterVisitor.ctx.mv;
// Defensive: ensure operand is a ListNode with a StringNode element
String argument = "";
if (node.operand instanceof ListNode listNode && !listNode.elements.isEmpty()
&& listNode.elements.getFirst() instanceof StringNode stringNode) {
argument = stringNode.value;
}
if (CompilerOptions.DEBUG_ENABLED) emitterVisitor.ctx.logDebug("visit diamond " + argument);
if (argument.isEmpty() || argument.equals("<>")) {
// Determine whether this is readline (<> or <<>>) or glob (<*.t>, <$var/*.t>).
// After interpolation, glob patterns may produce non-StringNode operands
// (e.g., BinaryOperatorNode for concatenation like $var . "/*.t").
boolean isReadline = false;
if (node.operand instanceof ListNode listNode) {
if (listNode.elements.isEmpty()) {
isReadline = true;
} else if (listNode.elements.getFirst() instanceof StringNode stringNode) {
isReadline = stringNode.value.isEmpty() || stringNode.value.equals("<>");
}
// If element is not a StringNode, it's an interpolated glob pattern → NOT readline
}
if (CompilerOptions.DEBUG_ENABLED) emitterVisitor.ctx.logDebug("visit diamond isReadline=" + isReadline);
if (isReadline) {
// Handle null filehandle: <> <<>>
node.operand.accept(emitterVisitor.with(RuntimeContextType.SCALAR));
emitterVisitor.pushCallContext();
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public final class Configuration {
* Automatically populated by Gradle/Maven during build.
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
public static final String gitCommitId = "28cf7aa5c";
public static final String gitCommitId = "98a17e401";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
Expand Down
70 changes: 65 additions & 5 deletions src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ static ListNode consumeArgsWithPrototype(Parser parser, String prototype, boolea
// element.setAnnotation("context", "LIST");
// }
} else {
parsePrototypeArguments(parser, args, prototype);
parsePrototypeArguments(parser, args, prototype, hasParentheses);

// Check for too many arguments without parentheses only if prototype expects 2+ args
if (!hasParentheses && countPrototypeArgs(prototype) >= 2) {
Expand Down Expand Up @@ -328,8 +328,9 @@ private static boolean handleOpeningParenthesis(Parser parser) {
* @param parser The parser instance
* @param args The argument list to populate
* @param prototype The prototype string to parse
* @param hasParentheses Whether the function was called with explicit parentheses
*/
private static void parsePrototypeArguments(Parser parser, ListNode args, String prototype) {
private static void parsePrototypeArguments(Parser parser, ListNode args, String prototype, boolean hasParentheses) {
boolean isOptional = false;
boolean needComma = false;
int skipCount = 0; // Number of prototype characters to skip (for flattened my/our/state)
Expand Down Expand Up @@ -382,7 +383,7 @@ private static void parsePrototypeArguments(Parser parser, ListNode args, String
needComma = true;
}
case '\\' -> {
i = handleBackslashArgument(parser, args, prototype, i + 1, isOptional, needComma);
i = handleBackslashArgument(parser, args, prototype, i + 1, isOptional, needComma, hasParentheses);
needComma = true;
}
case ',' -> {
Expand Down Expand Up @@ -747,7 +748,7 @@ private static Node unwrapUnaryPlus(Node arg, char refType) {
}

private static int handleBackslashArgument(Parser parser, ListNode args, String prototype, int prototypeIndex,
boolean isOptional, boolean needComma) {
boolean isOptional, boolean needComma, boolean hasParentheses) {
if (prototypeIndex >= prototype.length()) {
parser.throwError("syntax error, incomplete backslash reference in prototype");
}
Expand All @@ -762,7 +763,22 @@ private static int handleBackslashArgument(Parser parser, ListNode args, String
parser.parsingTakeReference = true;
}

Node referenceArg = parseArgumentWithComma(parser, isOptional, needComma, expectedType);
// Parse the backslash-prototype argument.
// With parentheses: always parse at comma precedence (level 5).
// Without parentheses:
// - Single-arg prototypes (e.g. \[$@%*], \$): parse at named-unary precedence
// so operators like && and == are NOT consumed. Example:
// tied *STDOUT && $cond → (tied *STDOUT) && $cond
// - Multi-arg prototypes (e.g. \$$, \$;$): parse at comma precedence
// so assignment and other operators ARE consumed. Example:
// sreftest my $a = 'val', $i++ → sreftest(\(my $a = 'val'), $i++)
Node referenceArg;
boolean useNamedUnary = !hasParentheses && countPrototypeArgs(prototype) <= 1;
if (useNamedUnary) {
referenceArg = parseBackslashArgWithComma(parser, isOptional, needComma, expectedType);
} else {
referenceArg = parseArgumentWithComma(parser, isOptional, needComma, expectedType);
}

// Restore flag
parser.parsingTakeReference = oldParsingTakeReference;
Expand Down Expand Up @@ -867,6 +883,50 @@ private static Node parseRequiredArgument(Parser parser, boolean isOptional) {
return expr;
}

/**
* Parses a backslash-prototype argument at named-unary precedence.
*
* <p>Backslash prototypes like {@code \[$@%*]} expect a single variable term.
* In Perl, these parse at named-unary precedence (between "isa" and shift operators),
* so operators like {@code &&}, {@code ||}, {@code ==}, {@code <} are NOT consumed,
* but arithmetic operators like {@code +}, {@code *}, {@code >>} ARE consumed.</p>
*
* @param parser The parser instance
* @param isOptional Whether the argument is optional
* @param needComma Whether a comma is required before the argument
* @param expectedType Description of the expected argument type for error messages
* @return The parsed argument node, or null if parsing failed and the argument was optional
*/
private static Node parseBackslashArgWithComma(Parser parser, boolean isOptional, boolean needComma, String expectedType) {
if (isArgumentTerminator(parser)) {
if (!isOptional) {
throwNotEnoughArgumentsError(parser);
}
return null;
}

if (needComma && !consumeCommaIfPresent(parser, isOptional)) {
return null;
}

if (isArgumentTerminator(parser)) {
if (isOptional) {
return null;
}
throwNotEnoughArgumentsError(parser);
}

// Parse at named-unary precedence (level 15, same as "isa")
// This ensures that comparison and logical operators are NOT consumed as part of the argument
Node expr = parser.parseExpression(parser.getPrecedence("isa"));
if (expr == null) {
if (!isOptional) {
throwNotEnoughArgumentsError(parser);
}
}
return expr;
}

/**
* Checks if there are consecutive commas (like ", ,") which should be a syntax error.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,15 @@ public static Node parseRawString(Parser parser, String operator) {
case "tr":
case "y":
return parseTransliteration(parser.ctx, rawStr);
case "<>": {
// In Perl, <$var/*.t> uses double-quote interpolation (like qq//)
// before passing to glob(). This ensures variables are interpolated.
Node interpolated = StringDoubleQuoted.parseDoubleQuotedString(
parser.ctx, rawStr, true, true, false, parser.getHeredocNodes(), parser);
ListNode diamondList = new ListNode(rawStr.index);
diamondList.elements.add(interpolated);
return new OperatorNode("<>", diamondList, rawStr.index);
}
}

ListNode list = new ListNode(rawStr.index);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
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.io.*;
import org.perlonjava.runtime.nativ.ffm.FFMPosix;
import org.perlonjava.runtime.perlmodule.Warnings;
import org.perlonjava.runtime.runtimetypes.*;
Expand Down Expand Up @@ -276,8 +273,16 @@ public static RuntimeScalar fileTest(String operator, RuntimeScalar fileHandle)

// 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();
while (true) {
if (innerHandle instanceof LayeredIOHandle lh) {
innerHandle = lh.getDelegate();
} else if (innerHandle instanceof DupIOHandle dh) {
innerHandle = dh.getDelegate();
} else if (innerHandle instanceof BorrowedIOHandle bh) {
innerHandle = bh.getDelegate();
} else {
break;
}
}
if (innerHandle instanceof CustomFileChannel cfc) {
// Special handling for -T/-B on filehandles: check from current position
Expand Down
17 changes: 11 additions & 6 deletions src/main/java/org/perlonjava/runtime/operators/Stat.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
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.io.*;
import org.perlonjava.runtime.nativ.NativeUtils;
import org.perlonjava.runtime.nativ.ffm.FFMPosix;
import org.perlonjava.runtime.nativ.ffm.FFMPosixInterface;
Expand Down Expand Up @@ -174,8 +171,16 @@ public static RuntimeList stat(RuntimeScalar arg) {
return res;
}
IOHandle innerHandle = fh.ioHandle;
while (innerHandle instanceof LayeredIOHandle lh) {
innerHandle = lh.getDelegate();
while (true) {
if (innerHandle instanceof LayeredIOHandle lh) {
innerHandle = lh.getDelegate();
} else if (innerHandle instanceof DupIOHandle dh) {
innerHandle = dh.getDelegate();
} else if (innerHandle instanceof BorrowedIOHandle bh) {
innerHandle = bh.getDelegate();
} else {
break;
}
}
if (innerHandle instanceof CustomFileChannel cfc) {
Path path = cfc.getFilePath();
Expand Down
19 changes: 15 additions & 4 deletions src/main/java/org/perlonjava/runtime/operators/TieOperators.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,13 @@ public static RuntimeScalar tie(int ctx, RuntimeBase... scalars) {
RuntimeGlob glob = variable.globDeref();
RuntimeIO previousValue = (RuntimeIO) glob.IO.value;
glob.IO.type = TIED_SCALAR;
glob.IO.value = new TieHandle(className, previousValue, self);
TieHandle tieHandle = new TieHandle(className, previousValue, self);
glob.IO.value = tieHandle;
// Update selectedHandle so that `print` without explicit filehandle
// goes through the tied handle (e.g., Test2::Plugin::IOEvents)
if (previousValue == RuntimeIO.selectedHandle) {
RuntimeIO.selectedHandle = tieHandle;
}
}
default -> {
return scalarUndef;
Expand Down Expand Up @@ -156,11 +162,16 @@ public static RuntimeScalar untie(int ctx, RuntimeBase... scalars) {
RuntimeGlob glob = variable.globDeref();
RuntimeScalar IO = glob.IO;
if (IO.type == TIED_SCALAR) {
TieHandle.tiedUntie((TieHandle) IO.value);
TieHandle.tiedDestroy((TieHandle) IO.value);
RuntimeIO previousValue = ((TieHandle) IO.value).getPreviousValue();
TieHandle currentTieHandle = (TieHandle) IO.value;
TieHandle.tiedUntie(currentTieHandle);
TieHandle.tiedDestroy(currentTieHandle);
RuntimeIO previousValue = currentTieHandle.getPreviousValue();
IO.type = 0; // XXX there is no type defined for IO handles
IO.value = previousValue;
// Restore selectedHandle if it pointed to the tied handle
if (currentTieHandle == RuntimeIO.selectedHandle) {
RuntimeIO.selectedHandle = previousValue;
}
}
return scalarTrue;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.perlonjava.runtime.perlmodule;

import org.perlonjava.runtime.runtimetypes.GlobalVariable;
import org.perlonjava.runtime.runtimetypes.RuntimeArray;
import org.perlonjava.runtime.runtimetypes.RuntimeHash;
import org.perlonjava.runtime.runtimetypes.RuntimeList;
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;
import org.perlonjava.runtime.runtimetypes.SystemUtils;
Expand Down Expand Up @@ -354,7 +356,11 @@ public static RuntimeList file_name_is_absolute(RuntimeArray args, int ctx) {
* @return A {@link RuntimeList} containing the directories in the PATH.
*/
public static RuntimeList path(RuntimeArray args, int ctx) {
String path = System.getenv("PATH");
// Read PATH from Perl's %ENV (not Java's System.getenv) so that
// modifications to $ENV{PATH} in Perl code are respected.
RuntimeHash perlEnv = GlobalVariable.getGlobalHash("main::ENV");
RuntimeScalar pathScalar = perlEnv.get(new RuntimeScalar("PATH"));
String path = pathScalar.getDefinedBoolean() ? pathScalar.toString() : System.getenv("PATH");
String[] paths = path != null ? path.split(File.pathSeparator) : new String[0];
List<RuntimeScalar> pathList = new ArrayList<>();
for (String p : paths) {
Expand Down
Loading
Loading