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
24 changes: 24 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,30 @@ The runner:
- Has a 300s timeout per test
- Reports pass/fail counts in format: `passed/total`
- Saves results to `test_results_YYYYMMDD_HHMMSS.txt`
- Sets required environment variables automatically (see below)

#### Running Tests Directly (without perl_test_runner.pl)

If you run tests directly with `./jperl`, you may need to set these environment variables:

```bash
# For tests that use unimplemented features (re/pat.t, op/pack.t, etc.)
# Without this, unimplemented features cause fatal errors
export JPERL_UNIMPLEMENTED=warn

# For memory-intensive tests (re/pat.t, op/repeat.t, op/list.t)
# Increases JVM stack size to prevent StackOverflowError
export JPERL_OPTS="-Xss256m"

# Skip tests with 300KB+ strings that crash the JVM
export PERL_SKIP_BIG_MEM_TESTS=1

# Example: running re/pat.t directly
cd perl5_t/t
JPERL_UNIMPLEMENTED=warn JPERL_OPTS="-Xss256m" PERL_SKIP_BIG_MEM_TESTS=1 ../../jperl re/pat.t
```

The perl_test_runner.pl sets these automatically based on the test file being run.

### Git Workflow

Expand Down
30 changes: 30 additions & 0 deletions src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.perlonjava.runtime.io;

import org.perlonjava.runtime.runtimetypes.GlobalVariable;
import org.perlonjava.runtime.runtimetypes.RuntimeHash;
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;
import org.perlonjava.runtime.runtimetypes.RuntimeScalarCache;

Expand Down Expand Up @@ -91,6 +93,9 @@ private void setupProcess(ProcessBuilder processBuilder) throws IOException {
String userDir = System.getProperty("user.dir");
processBuilder.directory(new File(userDir));

// Copy %ENV to the subprocess environment
copyPerlEnvToProcessBuilder(processBuilder);

// Start the process
process = processBuilder.start();

Expand Down Expand Up @@ -348,4 +353,29 @@ public RuntimeScalar sysread(int length) {
return new RuntimeScalar(); // undef
}
}

/**
* Copies the Perl %ENV hash to the ProcessBuilder environment.
* This ensures that changes to %ENV in Perl are reflected in child processes.
*
* @param processBuilder The ProcessBuilder to update
*/
private void copyPerlEnvToProcessBuilder(ProcessBuilder processBuilder) {
try {
RuntimeHash envHash = GlobalVariable.getGlobalHash("main::ENV");
java.util.Map<String, String> pbEnv = processBuilder.environment();

// Clear the inherited environment and replace with Perl's %ENV
pbEnv.clear();

for (java.util.Map.Entry<String, RuntimeScalar> entry : envHash.elements.entrySet()) {
String value = entry.getValue().toString();
if (value != null) {
pbEnv.put(entry.getKey(), value);
}
}
} catch (Exception e) {
// If we can't access %ENV, just use inherited environment (default behavior)
}
}
}
30 changes: 30 additions & 0 deletions src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.perlonjava.runtime.io;

import org.perlonjava.runtime.runtimetypes.GlobalVariable;
import org.perlonjava.runtime.runtimetypes.RuntimeHash;
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;
import org.perlonjava.runtime.runtimetypes.RuntimeScalarCache;

Expand Down Expand Up @@ -150,6 +152,9 @@ private void setupProcess(ProcessBuilder processBuilder) throws IOException {
String userDir = System.getProperty("user.dir");
processBuilder.directory(new File(userDir));

// Copy %ENV to the subprocess environment
copyPerlEnvToProcessBuilder(processBuilder);

// Start the process
process = processBuilder.start();

Expand Down Expand Up @@ -384,4 +389,29 @@ public RuntimeScalar syswrite(String data) {
return new RuntimeScalar(); // undef
}
}

/**
* Copies the Perl %ENV hash to the ProcessBuilder environment.
* This ensures that changes to %ENV in Perl are reflected in child processes.
*
* @param processBuilder The ProcessBuilder to update
*/
private void copyPerlEnvToProcessBuilder(ProcessBuilder processBuilder) {
try {
RuntimeHash envHash = GlobalVariable.getGlobalHash("main::ENV");
java.util.Map<String, String> pbEnv = processBuilder.environment();

// Clear the inherited environment and replace with Perl's %ENV
pbEnv.clear();

for (java.util.Map.Entry<String, RuntimeScalar> entry : envHash.elements.entrySet()) {
String value = entry.getValue().toString();
if (value != null) {
pbEnv.put(entry.getKey(), value);
}
}
} catch (Exception e) {
// If we can't access %ENV, just use inherited environment (default behavior)
}
}
}
21 changes: 21 additions & 0 deletions src/main/java/org/perlonjava/runtime/operators/IOOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -2211,6 +2211,27 @@ public static RuntimeIO openFileHandleDup(String fileName, String mode) {
break;
default:
// Try to look up as a global filehandle
// First, try the current package if no :: qualifier is present
if (!fileName.contains("::")) {
// Use RuntimeCode.getCurrentPackage() which uses caller() to determine
// the current package - this works for both JVM-compiled and interpreter code
String currentPkg = RuntimeCode.getCurrentPackage();
// Remove trailing "::" for consistent naming
if (currentPkg.endsWith("::")) {
currentPkg = currentPkg.substring(0, currentPkg.length() - 2);
}
if (currentPkg != null && !currentPkg.isEmpty() && !currentPkg.equals("main")) {
String currentPkgName = currentPkg + "::" + fileName;
RuntimeGlob currentGlob = GlobalVariable.getGlobalIO(currentPkgName);
if (currentGlob != null) {
sourceHandle = currentGlob.getRuntimeIO();
if (sourceHandle != null && sourceHandle.ioHandle != null) {
break; // Found it in current package
}
}
}
}
// Fall back to main:: or fully qualified name
String normalizedName = fileName.contains("::") ? fileName : "main::" + fileName;
RuntimeGlob glob = GlobalVariable.getGlobalIO(normalizedName);
if (glob != null) {
Expand Down
14 changes: 12 additions & 2 deletions src/main/java/org/perlonjava/runtime/operators/SystemOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -557,9 +557,19 @@ private static RuntimeScalar completeForkOpen(List<String> flattenedArgs, boolea
try {
flushAllHandles();

// Build the command
// Build the command - mirror the logic from exec() for consistency
List<String> command;
if (!hasHandle && flattenedArgs.size() == 1) {
if (hasHandle && flattenedArgs.size() >= 2) {
// Indirect object syntax: exec { $program } @args
// flattenedArgs[0] is the program from the indirect object
// flattenedArgs[1:] are the arguments from @args
// In Perl, @args[0] becomes argv[0] (process name), @args[1:] are actual arguments
// Java's ProcessBuilder can't set argv[0] separately, so we skip it
String program = flattenedArgs.get(0);
command = new ArrayList<>();
command.add(program);
command.addAll(flattenedArgs.subList(2, flattenedArgs.size()));
} else if (!hasHandle && flattenedArgs.size() == 1) {
String cmdStr = flattenedArgs.getFirst();
if (SHELL_METACHARACTERS.matcher(cmdStr).find()) {
// Use shell for metacharacters
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ public static RuntimeList canonpath(RuntimeArray args, int ctx) {
String quotedSeparator = Matcher.quoteReplacement(File.separator);
String canonPath = path.replaceAll("[/\\\\]+", quotedSeparator)
.replaceAll(Pattern.quote(File.separator) + "\\." + Pattern.quote(File.separator), quotedSeparator);

// Remove leading ./ unless the path is exactly "./"
// This matches Perl's File::Spec::Unix behavior
if (!canonPath.equals("." + File.separator)) {
while (canonPath.startsWith("." + File.separator)) {
canonPath = canonPath.substring(2);
}
}

return new RuntimeScalar(canonPath).getList();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public class RegexPreprocessor {
static int captureGroupCount;
static boolean deferredUnicodePropertyEncountered;
static boolean inlinePFlagEncountered;
static boolean branchResetEncountered;

static void markDeferredUnicodePropertyEncountered() {
deferredUnicodePropertyEncountered = true;
Expand All @@ -68,6 +69,10 @@ static boolean hadInlinePFlag() {
return inlinePFlagEncountered;
}

static boolean hadBranchReset() {
return branchResetEncountered;
}

/**
* Preprocesses a given regex string to make it compatible with Java's regex engine.
* This involves handling various constructs and escape sequences that Java does not
Expand All @@ -82,6 +87,7 @@ static String preProcessRegex(String s, RegexFlags regexFlags) {
captureGroupCount = 0;
deferredUnicodePropertyEncountered = false;
inlinePFlagEncountered = false;
branchResetEncountered = false;

// First, escape invalid quantifier braces (Perl compatibility)
// DISABLED: Causes test regressions - needs more work
Expand Down Expand Up @@ -1153,6 +1159,9 @@ private static int handleNamedCapture(int c, String s, int offset, int length, S
* @return New offset after processing the branch reset group
*/
private static int handleBranchReset(String s, int offset, int length, StringBuilder sb, RegexFlags regexFlags) {
// Mark that this pattern uses branch reset
branchResetEncountered = true;

// Save the starting group count
int startGroupCount = captureGroupCount;

Expand Down
14 changes: 13 additions & 1 deletion src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ protected boolean removeEldestEntry(Map.Entry<String, RuntimeRegex> eldest) {
private boolean matched = false;
private boolean hasCodeBlockCaptures = false; // True if regex has (?{...}) code blocks
private boolean deferredUserDefinedUnicodeProperties = false;
private boolean hasBranchReset = false; // True if pattern uses (?|...) branch reset

public RuntimeRegex() {
this.regexFlags = null;
Expand Down Expand Up @@ -145,6 +146,7 @@ public static RuntimeRegex compile(String patternString, String modifiers) {
// These need to be resolved later, once the corresponding Perl subs are defined.
regex.deferredUserDefinedUnicodeProperties = RegexPreprocessor.hadDeferredUnicodePropertyEncountered();
regex.hasPreservesMatch = regex.regexFlags.preservesMatch() || RegexPreprocessor.hadInlinePFlag();
regex.hasBranchReset = RegexPreprocessor.hadBranchReset();

regex.patternString = patternString;

Expand Down Expand Up @@ -620,7 +622,17 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc
if (ctx == RuntimeContextType.LIST) {
for (int i = 1; i <= captureCount; i++) {
String matchedStr = matcher.group(i);
if (matchedStr != null) {
if (regex.hasBranchReset) {
// For branch reset patterns (?|...), skip null groups
// because Java creates separate groups for each alternative
// but Perl reuses group numbers across alternatives
if (matchedStr != null) {
matchedGroups.add(new RuntimeScalar(matchedStr));
}
} else {
// Include undef for groups that didn't participate in the match
// This is important for patterns like m{^(.*/)?(.*)}s where
// the optional group returns undef when it doesn't match
matchedGroups.add(new RuntimeScalar(matchedStr));
}
}
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,11 @@ public RuntimeArray getGlobArray() {
}

public RuntimeGlob setIO(RuntimeScalar io) {
// Check if the current IO is the selected handle - if so, update it
RuntimeIO oldIO = null;
if (this.IO.value instanceof RuntimeIO) {
oldIO = (RuntimeIO) this.IO.value;
}
// If IO slot is tied (TIED_SCALAR with TieHandle), replace it entirely
// Otherwise use set() to modify in place, preserving sharing with detached copies
if (this.IO.type == RuntimeScalarType.TIED_SCALAR) {
Expand All @@ -432,13 +437,22 @@ public RuntimeGlob setIO(RuntimeScalar io) {
// If the IO scalar contains a RuntimeIO, set its glob name
if (io.value instanceof RuntimeIO runtimeIO) {
runtimeIO.globName = this.globName;
// Update selectedHandle if the old IO was the selected handle
if (oldIO != null && oldIO == RuntimeIO.selectedHandle) {
RuntimeIO.selectedHandle = runtimeIO;
}
}
return this;
}

public RuntimeGlob setIO(RuntimeIO io) {
// Set the glob name in the RuntimeIO for proper stringification
io.globName = this.globName;
// Check if the current IO is the selected handle - if so, update it
RuntimeIO oldIO = null;
if (this.IO.value instanceof RuntimeIO) {
oldIO = (RuntimeIO) this.IO.value;
}
// If IO slot is tied (TIED_SCALAR with TieHandle), replace it entirely
// Otherwise modify in place, preserving sharing with detached copies
if (this.IO.type == RuntimeScalarType.TIED_SCALAR) {
Expand All @@ -447,6 +461,12 @@ public RuntimeGlob setIO(RuntimeIO io) {
this.IO.type = RuntimeScalarType.GLOB; // RuntimeIO is stored as GLOB type
this.IO.value = io;
}
// Update selectedHandle if the old IO was the selected handle
// This ensures that when STDOUT is redirected, print without explicit
// filehandle uses the new handle
if (oldIO != null && oldIO == RuntimeIO.selectedHandle) {
RuntimeIO.selectedHandle = io;
}
return this;
}

Expand Down
15 changes: 15 additions & 0 deletions src/main/perl/lib/Test/Harness.pm
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,10 @@ sub _new_harness {
sub _filtered_inc {
my @inc = grep { !ref } @INC; #28567

# PerlOnJava: Filter out jar: paths - these are internal markers for
# modules bundled in the JAR and don't exist as filesystem directories
@inc = grep { !/^jar:/ } @inc;

if (IS_VMS) {

# VMS has a 255-byte limit on the length of %ENV entries, so
Expand Down Expand Up @@ -279,6 +283,15 @@ sub _filtered_inc {
shift @default_inc while @default_inc and $seen{ $default_inc[0] };
}

# Convert relative paths to absolute paths so they work in child processes
# that may run from a different directory
require Cwd;
@new_inc = map {
# Skip if already absolute or doesn't exist
($_ =~ m{^/} || $_ =~ m{^[A-Za-z]:}) ? $_ :
(-e $_) ? Cwd::abs_path($_) // $_ : $_
} @new_inc;

return @new_inc;
}

Expand All @@ -297,6 +310,8 @@ sub _filtered_inc {

# Avoid using -l for the benefit of Perl 6
chomp( @inc = `"$perl" -e "print join qq[\\n], \@INC, q[]"` );
# PerlOnJava: Filter out jar: paths from default @INC
@inc = grep { !/^jar:/ } @inc;
return @inc;
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/main/perl/lib/blib.pm
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ sub import
if (-d $blib && -d $blib_arch && -d $blib_lib)
{
unshift(@INC,$blib_arch,$blib_lib);
# PerlOnJava: Also set PERL5LIB so child processes can find modules.
# This is needed because PerlOnJava can't use fork() to share address space.
my $sep = $^O eq 'MSWin32' ? ';' : ':';
my $new_perl5lib = join($sep, $blib_arch, $blib_lib);
if (exists $ENV{PERL5LIB} && defined $ENV{PERL5LIB} && $ENV{PERL5LIB} ne '') {
$ENV{PERL5LIB} = $new_perl5lib . $sep . $ENV{PERL5LIB};
} else {
$ENV{PERL5LIB} = $new_perl5lib;
}
warn "Using $blib\n" if $Verbose;
return;
}
Expand Down
Loading