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
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int callContext, String subroutineName) {
// Track interpreter state for stack traces
String framePackageName = code.packageName != null ? code.packageName : "main";
String frameSubName = subroutineName != null ? subroutineName : (code.subName != null ? code.subName : "(eval)");
// Prefer code.subName (set by set_subname) over passed subroutineName
// This ensures caller() returns the name set by set_subname()
String frameSubName = code.subName != null ? code.subName : (subroutineName != null ? subroutineName : "(eval)");
// Get PC holder for direct updates (avoids ThreadLocal lookups in hot loop)
int[] pcHolder = InterpreterState.push(code, framePackageName, frameSubName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1217,7 +1217,12 @@ private static void visitLength(BytecodeCompiler bc, OperatorNode node) {
}

private static void visitDiamond(BytecodeCompiler bc, OperatorNode node) {
String argument = ((StringNode) ((ListNode) node.operand).elements.getFirst()).value;
// 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 (argument.isEmpty() || argument.equals("<>")) {
bc.compileNode(node.operand, -1, RuntimeContextType.SCALAR);
int fhReg = bc.lastResultReg;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,14 @@ static void handleNextOperator(EmitterContext ctx, OperatorNode node) {

// Initialize label string for labeled loops
String labelStr = null;
ListNode labelNode = (ListNode) node.operand;
// Defensive: ensure operand is a ListNode (parser should always create ListNode here)
ListNode labelNode;
if (node.operand instanceof ListNode) {
labelNode = (ListNode) node.operand;
} else {
// Wrap non-ListNode in a ListNode to handle edge cases
labelNode = ListNode.makeList(node.operand);
}
if (!labelNode.elements.isEmpty()) {
// Handle 'next' with a label.
Node arg = labelNode.elements.getFirst();
Expand Down
49 changes: 47 additions & 2 deletions src/main/java/org/perlonjava/backend/jvm/EmitOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,13 @@ static void handleSystemBuiltin(EmitterVisitor emitterVisitor, OperatorNode node
// static RuntimeBase reverse(RuntimeBase value, int ctx)
if (CompilerOptions.DEBUG_ENABLED) emitterVisitor.ctx.logDebug("handleSystemBuiltin " + node);

ListNode operand = (ListNode) node.operand;
// Defensive: ensure operand is a ListNode
ListNode operand;
if (node.operand instanceof ListNode) {
operand = (ListNode) node.operand;
} else {
operand = ListNode.makeList(node.operand);
}
boolean hasHandle = false;
if (operand.handle != null) {
// `system {handle} LIST`
Expand Down Expand Up @@ -584,7 +590,12 @@ 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;
String argument = ((StringNode) ((ListNode) node.operand).elements.getFirst()).value;
// 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("<>")) {
// Handle null filehandle: <> <<>>
Expand Down Expand Up @@ -1030,6 +1041,40 @@ static void handleTimeRelatedOperator(EmitterVisitor emitterVisitor, OperatorNod
emitOperator(node, emitterVisitor);
}

/**
* Handle the caller() operator with __SUB__ support for set_subname.
* Pushes: args, context, __SUB__
*/
static void handleCallerOperator(EmitterVisitor emitterVisitor, OperatorNode node) {
if (node.operand != null) {
node.operand.accept(emitterVisitor.with(RuntimeContextType.LIST));
}
emitterVisitor.pushCallContext();

// Push __SUB__ for set_subname support
MethodVisitor mv = emitterVisitor.ctx.mv;
String className = emitterVisitor.ctx.javaClassInfo.javaClassName;

// Load 'this' (the current RuntimeCode instance)
mv.visitVarInsn(Opcodes.ALOAD, 0);

// Retrieve this.__SUB__
mv.visitFieldInsn(Opcodes.GETFIELD,
className,
"__SUB__",
"Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;");

// Create Perl undef if null (for code not inside a subroutine)
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/runtimetypes/RuntimeCode",
"selfReferenceMaybeNull",
"(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;",
false);

emitOperator(node, emitterVisitor);
}

static void handlePrototypeOperator(EmitterVisitor emitterVisitor, OperatorNode node) {
node.operand.accept(emitterVisitor.with(RuntimeContextType.SCALAR));
emitterVisitor.pushCurrentPackage();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,9 @@ public static void emitOperatorNode(EmitterVisitor emitterVisitor, OperatorNode
case "time", "wait" -> EmitOperator.handleTimeOperator(emitterVisitor, node);
case "wantarray" -> EmitOperator.handleWantArrayOperator(emitterVisitor, node);
case "undef" -> EmitOperator.handleUndefOperator(emitterVisitor, node);
case "gmtime", "localtime", "caller", "reset", "select", "times" ->
case "gmtime", "localtime", "reset", "select", "times" ->
EmitOperator.handleTimeRelatedOperator(emitterVisitor, node);
case "caller" -> EmitOperator.handleCallerOperator(emitterVisitor, node);
case "prototype" -> EmitOperator.handlePrototypeOperator(emitterVisitor, node);
case "require" -> EmitOperator.handleRequireOperator(emitterVisitor, node);
case "doFile" -> EmitOperator.handleDoFileOperator(emitterVisitor, node);
Expand Down
20 changes: 16 additions & 4 deletions src/main/java/org/perlonjava/backend/jvm/EmitRegex.java
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,10 @@ static void handleSystemCommand(EmitterVisitor emitterVisitor, OperatorNode node
* Example: $string =~ tr/abc/def/
*/
static void handleTransliterate(EmitterVisitor emitterVisitor, OperatorNode node) {
ListNode operand = (ListNode) node.operand;
// Defensive: ensure operand is a ListNode
ListNode operand = (node.operand instanceof ListNode)
? (ListNode) node.operand
: ListNode.makeList(node.operand);
EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR);

// Process the three required components: source, target, and flags
Expand Down Expand Up @@ -186,7 +189,10 @@ static void handleTransliterate(EmitterVisitor emitterVisitor, OperatorNode node
* Example: $string =~ s/pattern/replacement/
*/
static void handleReplaceRegex(EmitterVisitor emitterVisitor, OperatorNode node) {
ListNode operand = (ListNode) node.operand;
// Defensive: ensure operand is a ListNode
ListNode operand = (node.operand instanceof ListNode)
? (ListNode) node.operand
: ListNode.makeList(node.operand);
EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR);

// Process pattern, replacement, and flags
Expand Down Expand Up @@ -224,7 +230,10 @@ static void handleReplaceRegex(EmitterVisitor emitterVisitor, OperatorNode node)
* Example: qr/pattern/
*/
static void handleQuoteRegex(EmitterVisitor emitterVisitor, OperatorNode node) {
ListNode operand = (ListNode) node.operand;
// Defensive: ensure operand is a ListNode
ListNode operand = (node.operand instanceof ListNode)
? (ListNode) node.operand
: ListNode.makeList(node.operand);
EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR);

// Process pattern and flags
Expand All @@ -246,7 +255,10 @@ static void handleQuoteRegex(EmitterVisitor emitterVisitor, OperatorNode node) {
* Example: $string =~ m/pattern/
*/
static void handleMatchRegex(EmitterVisitor emitterVisitor, OperatorNode node) {
ListNode operand = (ListNode) node.operand;
// Defensive: ensure operand is a ListNode
ListNode operand = (node.operand instanceof ListNode)
? (ListNode) node.operand
: ListNode.makeList(node.operand);
EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR);

// Check if /o modifier is present
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 = "c4e439b01";
public static final String gitCommitId = "4473efe87";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,13 @@ private static Node parseIdentifier(Parser parser, int startIndex, LexerToken to
OperatorNode codeRef = new OperatorNode("&",
new IdentifierNode(coreGlobalName, startIndex),
startIndex);
// Defensive: ensure operand is a ListNode
ListNode operandList = (requireOp.operand instanceof ListNode)
? (ListNode) requireOp.operand
: ListNode.makeList(requireOp.operand);
return new BinaryOperatorNode("(",
codeRef,
(ListNode) requireOp.operand,
operandList,
startIndex);
}
}
Expand Down
27 changes: 25 additions & 2 deletions src/main/java/org/perlonjava/runtime/operators/Operator.java
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,8 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas
int size = args.length;
int offset = ((RuntimeScalar) args[1]).getInt();
// If length is not provided, use the rest of the string
int length = (size > 2) ? ((RuntimeScalar) args[2]).getInt() : strLength - offset;
boolean hasExplicitLength = size > 2;
int length = hasExplicitLength ? ((RuntimeScalar) args[2]).getInt() : strLength - offset;
String replacement = (size > 3) ? args[3].toString() : null;

// Store original offset and length for LValue creation
Expand All @@ -282,9 +283,31 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas
// Handle negative offsets (count from the end of the string)
if (offset < 0) {
offset = strLength + offset;
// When no explicit length is provided, Perl clips negative offsets to 0 (no warning)
// When explicit length IS provided, Perl warns and returns undef for too-negative offsets
if (offset < 0) {
if (hasExplicitLength) {
// Warn and return undef (same as positive offset out of bounds)
if (warnEnabled) {
WarnDie.warn(new RuntimeScalar("substr outside of string"),
RuntimeScalarCache.scalarEmptyString);
}
if (replacement != null) {
return new RuntimeScalar();
}
var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", originalOffset, originalLength);
lvalue.type = RuntimeScalarType.UNDEF;
lvalue.value = null;
return lvalue;
} else {
// Clip to 0 without warning
offset = 0;
}
}
}

if (offset < 0 || offset > strLength) {
// Only warn/error for positive offsets that exceed string length
if (offset > strLength) {
if (warnEnabled) {
WarnDie.warn(new RuntimeScalar("substr outside of string"),
RuntimeScalarCache.scalarEmptyString);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ public record OperatorHandler(String className, String methodName, int methodTyp
put("bless", "bless", "org/perlonjava/runtime/operators/ReferenceOperators", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;");
put("ref", "ref", "org/perlonjava/runtime/operators/ReferenceOperators", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;");

put("caller", "caller", "org/perlonjava/runtime/runtimetypes/RuntimeCode", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeList;");
put("caller", "callerWithSub", "org/perlonjava/runtime/runtimetypes/RuntimeCode", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;ILorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeList;");
put("reset", "reset", "org/perlonjava/runtime/operators/Operator", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeList;");
put("warn", "warn", "org/perlonjava/runtime/operators/WarnDie", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Ljava/lang/String;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;");
put("die", "die", "org/perlonjava/runtime/operators/WarnDie", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Ljava/lang/String;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;");
Expand Down
34 changes: 18 additions & 16 deletions src/main/java/org/perlonjava/runtime/operators/SystemOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,17 @@ private static CommandResult executeCommand(String command, boolean captureOutpu

if (captureOutput) {
// For backticks: capture stdout only, stderr already goes to terminal
// Read raw bytes to preserve exact output (including or excluding trailing newlines)
Thread stdoutThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(finalProcess.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
synchronized (finalOutput) {
finalOutput.append(line).append("\n");
}
try (java.io.InputStream is = finalProcess.getInputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
while ((bytesRead = is.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
synchronized (finalOutput) {
finalOutput.append(baos.toString());
}
} catch (IOException e) {
// Stream closed - this is normal when process terminates
Expand Down Expand Up @@ -438,25 +442,23 @@ private static RuntimeScalar completeForkOpen(List<String> flattenedArgs, boolea

Process process = processBuilder.start();

// Read all output
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
// Read all output as raw bytes to preserve exact output
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
try (java.io.InputStream is = process.getInputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
}
String capturedOutput = baos.toString();

// Wait for process to complete
int exitCode = process.waitFor();

// Set $? to the exit status
setGlobalVariable("main::?", String.valueOf(exitCode << 8));

// Remove trailing newline if present (to match Perl behavior for single-line output)
String capturedOutput = output.toString();

// Throw exception to return control to caller with captured output
throw new ForkOpenCompleteException(
process.pid(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ public static void initializeGlobals(CompilerOptions compilerOptions) {
GlobalVariable.getGlobalVariable(encodeSpecialVar("R")); // initialize $^R to "undef" - writable variable
GlobalVariable.getGlobalVariable(encodeSpecialVar("A")).set(""); // initialize $^A to "" - format accumulator variable
GlobalVariable.getGlobalVariable(encodeSpecialVar("P")).set(0); // initialize $^P to 0 - debugger flags
// Initialize $^I (in-place editing extension) from -i switch
if (compilerOptions.inPlaceEdit) {
GlobalVariable.getGlobalVariable(encodeSpecialVar("I")).set(
compilerOptions.inPlaceExtension != null ? compilerOptions.inPlaceExtension : "");
}
GlobalVariable.globalVariables.put(encodeSpecialVar("LAST_SUCCESSFUL_PATTERN"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_SUCCESSFUL_PATTERN));
GlobalVariable.globalVariables.put(encodeSpecialVar("LAST_FH"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_FH)); // $^LAST_FH
GlobalVariable.getGlobalVariable(encodeSpecialVar("UNICODE")).set(0); // initialize $^UNICODE to 0 - `-C` unicode flags
Expand Down
Loading
Loading