Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
6f90620
Fix Module::Runtime test failures: #line directive, hints hash, reloa…
fglock Mar 22, 2026
3f75402
Fix base.pm isa check and error message formatting
fglock Mar 22, 2026
d9b3ef9
Fix parent.pm tests: normalize old-style package separator and improv…
fglock Mar 22, 2026
d5c0919
Fix Module::Metadata tests: Unicode regex, File::Spec path handling
fglock Mar 22, 2026
2b8361e
Fix %main:: to include top-level packages in stash enumeration
fglock Mar 22, 2026
a4fd199
Fix substr() with negative offsets that overshoot string start
fglock Mar 22, 2026
a58b4cc
Fix regex /u flag: only enable Unicode character classes when requested
fglock Mar 22, 2026
ce00ee9
Fix cached require error message to include 'Compilation failed'
fglock Mar 22, 2026
dba7944
Fix version qv flag and stringify for decimal versions
fglock Mar 22, 2026
d1cf8a0
Fix version module: strip trailing zeros and reject math ops
fglock Mar 22, 2026
e475e1c
Fix File::Temp: TEMPLATE option and PERMS support
fglock Mar 22, 2026
3d750ed
Fix File::Temp cleanup when chdir'd or using relative paths
fglock Mar 22, 2026
50e7205
Fix anonymous glob slot dereferencing (${*$fh}, %{*$fh}, @{*$fh})
fglock Mar 22, 2026
1f22855
Fix File::Temp tests: fileno, autoflush, template path handling
fglock Mar 22, 2026
9bf62c2
Revert fileno synthetic fd change to fix io/perlio_leaks.t regressions
fglock Mar 22, 2026
252e357
Fix DateTime test failures: overload warnings and custom warning cate…
fglock Mar 22, 2026
1a3a509
Fix indirect object syntax with blocks for undefined barewords
fglock Mar 22, 2026
8aa8d62
Fix warnings::warnif to work with Test::Warnings warning capture
fglock Mar 22, 2026
f0df197
Add warnings scope design doc and infrastructure
fglock Mar 22, 2026
1af9e9e
Implement lexical warning scope propagation for warnif()
fglock Mar 22, 2026
02a1a0a
Fix runtime warning scope check in RuntimeIO
fglock Mar 22, 2026
9fc0431
Add architecture documentation
fglock Mar 23, 2026
38832fe
Fix stringConcatWarnUninitialized to avoid double FETCH on tied scalars
fglock Mar 23, 2026
8d45476
Fix tied variable FETCH/STORE semantics for chained assignments
fglock Mar 23, 2026
1e420c8
Fix regex /i flag to not affect Unicode properties
fglock Mar 23, 2026
ad8b5df
Revert tied variable and regex changes that caused test regressions
fglock Mar 23, 2026
0fe84a1
Restore StringOperators fix for tied scalar concat (fixes op/gmagic.t)
fglock Mar 23, 2026
7c9d597
Fix /i flag handling for Unicode properties in extended character cla…
fglock Mar 23, 2026
e209ba6
Fix extended char class negation with /i flag
fglock Mar 23, 2026
a2ce08d
Fix case-insensitive matching for extended char class ranges and escapes
fglock Mar 23, 2026
30c5386
Fix stdin hang when using -M without -e or script file
fglock Mar 23, 2026
b586e2b
Add deprecate.pm pragma stub
fglock Mar 23, 2026
9c983eb
Add $VERSION to core pragmas for CPAN compatibility
fglock Mar 23, 2026
f1869bd
Add base.pm and parent.pm stubs for CPAN detection
fglock Mar 23, 2026
62bdeed
Add Encode.pm stub and VERSION to POSIX.pm for CPAN detection
fglock Mar 23, 2026
1ad3f8f
Fix parsing of %{&{$code}} hash dereference
fglock Mar 23, 2026
739939e
Fix B module SVf_POK and CPAN::Meta::YAML refaddr
fglock Mar 23, 2026
a41676d
Add ExtUtils::MakeMaker and related modules for jcpan
fglock Mar 23, 2026
d7e53c6
Fix code references to survive stash deletion (Perl semantics)
fglock Mar 23, 2026
3e39116
Fix MM_PerlOnJava test target to include blib/lib in @INC via PERL5LIB
fglock Mar 23, 2026
2d9e752
Add blib.pm and fix MM_PerlOnJava to skip perllocal/packlist writes
fglock Mar 23, 2026
cd220c5
Fix -M module hang and add PERL5LIB to stub Makefile tests
fglock Mar 23, 2026
c76b817
Add deprecate.pm core module
fglock Mar 23, 2026
4276215
Update ExtUtils::MakeMaker to version 7.78
fglock Mar 23, 2026
d79a7d6
Implement lock() builtin as no-op for non-threaded Perl
fglock Mar 23, 2026
64b88c3
Merge fix/module-runtime-tests into fix/module-only-stdin-hang
fglock Mar 23, 2026
c0cc48f
Fix list slice semantics for empty lists
fglock Mar 23, 2026
5e955da
Fix list slice bytecode generation and add XSLoader.pm stub
fglock Mar 23, 2026
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
50 changes: 28 additions & 22 deletions src/main/java/org/perlonjava/app/cli/ArgumentParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,31 +43,37 @@ public static CompilerOptions parseArguments(String[] args) {

// If no code was provided and no filename, try reading from stdin
if (parsedArgs.code == null) {
try {
// Try to read from stdin - this will work for pipes, redirections, and interactive input
StringBuilder stdinContent = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

// Check if we're reading from a pipe/redirection vs interactive terminal
boolean isInteractive = System.console() != null;

if (isInteractive) {
// Interactive mode - prompt the user and read until EOF (Ctrl+D)
System.err.println("Enter Perl code (press Ctrl+D when done):");
}
// Check if we're reading from a pipe/redirection vs interactive terminal
boolean isInteractive = System.console() != null;

// If interactive and we have -M modules, just run them without waiting for stdin
// This matches Perl behavior: perl -MModule=args runs the module and exits
if (isInteractive && !parsedArgs.moduleUseStatements.isEmpty()) {
parsedArgs.code = ""; // Empty code, just run the use statements
} else {
try {
// Try to read from stdin - this will work for pipes, redirections, and interactive input
StringBuilder stdinContent = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

if (isInteractive) {
// Interactive mode - prompt the user and read until EOF (Ctrl+D)
System.err.println("Enter Perl code (press Ctrl+D when done):");
}

// Read from stdin regardless of whether it's interactive or not
String line;
while ((line = reader.readLine()) != null) {
stdinContent.append(line).append("\n");
}
// Read from stdin regardless of whether it's interactive or not
String line;
while ((line = reader.readLine()) != null) {
stdinContent.append(line).append("\n");
}

if (stdinContent.length() > 0) {
parsedArgs.code = stdinContent.toString();
parsedArgs.fileName = "-"; // Indicate that code came from stdin
if (stdinContent.length() > 0) {
parsedArgs.code = stdinContent.toString();
parsedArgs.fileName = "-"; // Indicate that code came from stdin
}
} catch (IOException e) {
// If we can't read from stdin, continue with normal error handling
}
} catch (IOException e) {
// If we can't read from stdin, continue with normal error handling
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3911,14 +3911,18 @@ void compileVariableReference(OperatorNode node, String op) {
// This will add the current package if no package is specified
subName = NameNormalizer.normalizeVariableName(subName, getCurrentPackage());

// Allocate register for code reference
// Cache the RuntimeScalar code reference at compile time.
// This matches Perl's behavior where the CV (code value) is cached
// in the compiled bytecode, surviving stash entry deletion.
RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRef(subName);

// Allocate register and load from constant pool
int rd = allocateOutputRegister();
int nameIdx = addToStringPool(subName);
int constIdx = addToConstantPool(codeRef);

// Emit LOAD_GLOBAL_CODE
emit(Opcodes.LOAD_GLOBAL_CODE);
emit(Opcodes.LOAD_CONST);
emitReg(rd);
emit(nameIdx);
emit(constIdx);

lastResultReg = rd;
} else if (node.operand instanceof BlockNode || node.operand instanceof OperatorNode) {
Expand Down
65 changes: 60 additions & 5 deletions src/main/java/org/perlonjava/backend/jvm/Dereference.java
Original file line number Diff line number Diff line change
Expand Up @@ -365,14 +365,69 @@ static void handleArrayElementOperator(EmitterVisitor emitterVisitor, BinaryOper
}
}
if (node.left instanceof ListNode list) { // ("a","b","c")[2]
// transform to: ["a","b","c"]->[2]
BinaryOperatorNode refNode = new BinaryOperatorNode("->",
new ArrayLiteralNode(list.elements, list.getIndex()),
node.right, node.tokenIndex);
refNode.accept(emitterVisitor);
// Use proper list slice semantics: evaluate list, then slice
// This differs from array dereference because empty list returns empty, not undef
if (CompilerOptions.DEBUG_ENABLED) emitterVisitor.ctx.logDebug("visit(BinaryOperatorNode) (list)[indices] - list slice");

// Evaluate the list
list.accept(emitterVisitor.with(RuntimeContextType.LIST));

// Convert to RuntimeList if not already (handles RuntimeScalar case)
emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"org/perlonjava/runtime/runtimetypes/RuntimeBase",
"getList",
"()Lorg/perlonjava/runtime/runtimetypes/RuntimeList;",
false);

// Save the list to a local variable before evaluating indices.
// This is necessary because indices may contain function calls that
// generate complex bytecode with exception handlers, and the JVM
// verifier requires consistent stack heights at merge points.
int listVar = emitterVisitor.ctx.symbolTable.allocateLocalVariable();
emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, listVar);

// Evaluate the indices
ListNode indices = ((ArrayLiteralNode) node.right).asListNode();
indices.accept(emitterVisitor.with(RuntimeContextType.LIST));

// Save indices to local variable too
int indicesVar = emitterVisitor.ctx.symbolTable.allocateLocalVariable();
emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, indicesVar);

// Load list and indices back, call RuntimeList.getSlice(indices)
emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, listVar);
emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, indicesVar);
emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"org/perlonjava/runtime/runtimetypes/RuntimeList",
"getSlice",
"(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;)Lorg/perlonjava/runtime/runtimetypes/RuntimeList;",
false);

// Handle context conversion
if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) {
emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeList",
"scalar", "()Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false);
} else if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) {
emitterVisitor.ctx.mv.visitInsn(Opcodes.POP);
}
return;
}

// For function calls and other expressions: (func())[index]
// We need to use list slice semantics to handle empty lists correctly.
// However, this should NOT apply to chained dereferences like $matrix[1][0]
// where the first [1] returns a scalar (array reference) and the second
// [0] should dereference it.
//
// List slice semantics apply when:
// 1. The left side is a ListNode (literal list) - handled above
// 2. The left side is a parenthesized function call (wantarray context)
//
// For now, we use the old transformation to ->[] for non-ListNode cases,
// as most cases are array dereferences, not list slices.
// TODO: Properly detect when the left side is a list-returning expression
// vs. a scalar-returning expression.

// default: call `->[]`
BinaryOperatorNode refNode = new BinaryOperatorNode("->", node.left, node.right, node.tokenIndex);
refNode.accept(emitterVisitor);
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 = "38832fe97";
public static final String gitCommitId = "427621554";

/**
* 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 @@ -201,6 +201,11 @@ public static String parseComplexIdentifierInner(Parser parser, boolean insideBr
if (insideBraces && firstChar == '*' && nextToken.text.equals("{")) {
return null; // Force fallback to expression parsing for glob dereference
}
// Special case: & followed by { is subroutine call when inside braces
// %{&{$code}} should be parsed as %{ &{$code} }, not %&{$code} (hash subscript on %&)
if (insideBraces && firstChar == '&' && nextToken.text.equals("{")) {
return null; // Force fallback to expression parsing for subroutine call
}
// Check if this is a leading single quote followed by an identifier ($'foo means $main::foo)
if (firstChar == '\'' && (nextToken.type == LexerTokenType.IDENTIFIER || nextToken.type == LexerTokenType.NUMBER)) {
// This is $'foo which means $main::foo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,10 @@ public record OperatorHandler(String className, String methodName, int methodTyp
"scalar",
Opcodes.INVOKEVIRTUAL,
"()Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"));

// Thread-related (no-op in non-threaded Perl)
put("lock", "lock", "org/perlonjava/runtime/operators/TieOperators", "(I[Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;");

operatorHandlers.put("each",
new OperatorHandler("org/perlonjava/runtime/runtimetypes/RuntimeBase",
"each",
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/org/perlonjava/runtime/operators/TieOperators.java
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,30 @@ public static RuntimeScalar tied(int ctx, RuntimeBase... scalars) {
}
return scalarUndef;
}

/**
* Implements Perl's lock() builtin function.
*
* <p>In threaded Perl, lock() places an advisory lock on a shared variable.
* In non-threaded Perl (and PerlOnJava), it's a no-op that returns its argument.</p>
*
* <p>The prototype for lock is \[$@%&*] so the argument is passed as a reference.</p>
*
* @param ctx the calling context
* @param scalars varargs where scalars[0] is a reference to the variable to lock
* @return for scalar refs, the dereferenced value; for arrays/hashes, the reference
*/
public static RuntimeScalar lock(int ctx, RuntimeBase... scalars) {
// No-op in non-threaded Perl - return the argument appropriately
if (scalars.length == 0) {
return scalarUndef;
}
RuntimeScalar variable = scalars[0].getFirst();
// For scalar references, dereference to get the value
// For other reference types (arrays, hashes), return the reference itself
return switch (variable.type) {
case REFERENCE -> variable.scalarDeref();
default -> variable;
};
}
}
2 changes: 2 additions & 0 deletions src/main/java/org/perlonjava/runtime/perlmodule/Base.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public static void initialize() {
Base base = new Base();
try {
base.registerMethod("import", "importBase", ";$");
// Set $VERSION so CPAN.pm can detect our bundled version
GlobalVariable.getGlobalVariable("base::VERSION").set(new RuntimeScalar("2.27"));
} catch (NoSuchMethodException e) {
System.err.println("Warning: Missing Base method: " + e.getMessage());
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/perlonjava/runtime/perlmodule/Parent.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public static void initialize() {
Parent parent = new Parent();
try {
parent.registerMethod("import", "importParent", ";$");
// Set $VERSION so CPAN.pm can detect our bundled version
GlobalVariable.getGlobalVariable("parent::VERSION").set(new RuntimeScalar("0.244"));
} catch (NoSuchMethodException e) {
System.err.println("Warning: Missing Parent method: " + e.getMessage());
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/org/perlonjava/runtime/perlmodule/Strict.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.perlonjava.runtime.perlmodule;

import org.perlonjava.frontend.semantic.ScopedSymbolTable;
import org.perlonjava.runtime.runtimetypes.GlobalVariable;
import org.perlonjava.runtime.runtimetypes.RuntimeArray;
import org.perlonjava.runtime.runtimetypes.RuntimeList;
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;
Expand Down Expand Up @@ -45,6 +46,8 @@ public static void initialize() {
try {
strict.registerMethod("import", "useStrict", ";$");
strict.registerMethod("unimport", "noStrict", ";$");
// Set $VERSION so CPAN.pm can detect our bundled version
GlobalVariable.getGlobalVariable("strict::VERSION").set(new RuntimeScalar("1.14"));
} catch (NoSuchMethodException e) {
System.err.println("Warning: Missing Strict method: " + e.getMessage());
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/perlonjava/runtime/perlmodule/Utf8.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public static void initialize() {
utf8.registerMethod("unicode_to_native", "unicodeToNative", "$");
utf8.registerMethod("is_utf8", "isUtf8", "$");
utf8.registerMethod("valid", "$");
// Set $VERSION so CPAN.pm can detect our bundled version
GlobalVariable.getGlobalVariable("utf8::VERSION").set(new RuntimeScalar("1.29"));
} catch (NoSuchMethodException e) {
System.err.println("Warning: Missing Utf8 method: " + e.getMessage());
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/perlonjava/runtime/perlmodule/Vars.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public static void initialize() {
Vars vars = new Vars();
try {
vars.registerMethod("import", "importVars", ";$");
// Set $VERSION so CPAN.pm can detect our bundled version
GlobalVariable.getGlobalVariable("vars::VERSION").set(new RuntimeScalar("1.05"));
} catch (NoSuchMethodException e) {
System.err.println("Warning: Missing vars method: " + e.getMessage());
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/perlonjava/runtime/perlmodule/Warnings.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public static void initialize() {
warnings.registerMethod("warn", "warn", "$;$");
warnings.registerMethod("warnif", "warnIf", "$;$");
warnings.registerMethod("register_categories", "registerCategories", ";@");
// Set $VERSION so CPAN.pm can detect our bundled version
GlobalVariable.getGlobalVariable("warnings::VERSION").set(new RuntimeScalar("1.74"));
} catch (NoSuchMethodException e) {
System.err.println("Warning: Missing Warnings method: " + e.getMessage());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public class GlobalVariable {
static final Map<String, RuntimeGlob> globalIORefs = new HashMap<>();
static final Map<String, RuntimeFormat> globalFormatRefs = new HashMap<>();

// Pinned code references: RuntimeScalars that were accessed at compile time
// and should survive stash deletion. This matches Perl's behavior where
// compiled bytecode holds direct references to CVs that survive stash deletion.
private static final Map<String, RuntimeScalar> pinnedCodeRefs = new HashMap<>();

// Stash aliasing: `*{Dst::} = *{Src::}` effectively makes Dst:: symbol table
// behave like Src:: for method lookup and stash operations.
// We keep this separate from globalCodeRefs/globalVariables so existing references
Expand Down Expand Up @@ -66,6 +71,7 @@ public static void resetAllGlobals() {
globalArrays.clear();
globalHashes.clear();
globalCodeRefs.clear();
pinnedCodeRefs.clear();
globalIORefs.clear();
globalFormatRefs.clear();
globalGlobs.clear();
Expand Down Expand Up @@ -323,11 +329,23 @@ public static RuntimeHash removeGlobalHash(String key) {

/**
* Retrieves a global code reference by its key, initializing it if necessary.
* The returned RuntimeScalar is also pinned, meaning it will survive stash deletion.
* This matches Perl's behavior where compiled bytecode holds direct references to CVs.
*
* @param key The key of the global code reference.
* @return The RuntimeScalar representing the global code reference.
*/
public static RuntimeScalar getGlobalCodeRef(String key) {
// First check if we have a pinned reference that survives stash deletion
RuntimeScalar pinned = pinnedCodeRefs.get(key);
if (pinned != null) {
// Also ensure it's in globalCodeRefs for normal lookups
if (!globalCodeRefs.containsKey(key)) {
globalCodeRefs.put(key, pinned);
}
return pinned;
}

RuntimeScalar var = globalCodeRefs.get(key);
if (var == null) {
var = new RuntimeScalar();
Expand All @@ -351,6 +369,10 @@ public static RuntimeScalar getGlobalCodeRef(String key) {
var.value = runtimeCode;
globalCodeRefs.put(key, var);
}

// Pin the RuntimeScalar so it survives stash deletion
pinnedCodeRefs.put(key, var);

return var;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,14 @@ public RuntimeScalar set(RuntimeGlob value) {
// Create ALIASES by making both names point to the same objects in the global maps
// This is the key difference from the old implementation which created references

// Alias the CODE slot: both names point to the same code reference
// Alias the CODE slot: Update the existing RuntimeScalar's value instead of replacing it.
// This is critical because compiled code may have cached references to the existing
// RuntimeScalar at compile time. Replacing the map entry would leave cached references
// pointing to the old (now orphaned) RuntimeScalar, causing calls to fail after
// the stash entry is deleted.
RuntimeScalar sourceCode = GlobalVariable.getGlobalCodeRef(globName);
GlobalVariable.globalCodeRefs.put(this.globName, sourceCode);
RuntimeScalar targetCode = GlobalVariable.getGlobalCodeRef(this.globName);
targetCode.set(sourceCode); // Copy value into existing RuntimeScalar
// Invalidate the method resolution cache
InheritanceResolver.invalidateCache();

Expand Down
Loading
Loading