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
846 changes: 719 additions & 127 deletions dev/cpan-reports/cpan-compatibility-fail.dat

Large diffs are not rendered by default.

300 changes: 277 additions & 23 deletions dev/cpan-reports/cpan-compatibility-pass.dat

Large diffs are not rendered by default.

1,168 changes: 1,007 additions & 161 deletions dev/cpan-reports/cpan-compatibility.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,22 @@ 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 = "c1e3e0acb";
public static final String gitCommitId = "ae3c753b5";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
* Automatically populated by Gradle/Maven during build.
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
public static final String gitCommitDate = "2026-04-21";
public static final String gitCommitDate = "2026-04-22";

/**
* Build timestamp in Perl 5 "Compiled at" format (e.g., "Apr 7 2026 11:20:00").
* Automatically populated by Gradle during build.
* Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at"
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
public static final String buildTimestamp = "Apr 21 2026 22:36:17";
public static final String buildTimestamp = "Apr 22 2026 10:10:47";

// Prevent instantiation
private Configuration() {
Expand Down
47 changes: 39 additions & 8 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -886,14 +886,26 @@ public static Class<?> evalStringHelper(RuntimeScalar code, String evalTag, Obje
String varNameWithoutSigil = entry.name().substring(1);
String fullName = packageName + "::" + varNameWithoutSigil;

// Only install the alias (and schedule cleanup) if no other
// alias is already in place for this key. See matching comment
// below in the BytecodeInterpreter path.
boolean installed = false;
if (runtimeValue instanceof RuntimeArray) {
GlobalVariable.globalArrays.put(fullName, (RuntimeArray) runtimeValue);
if (GlobalVariable.globalArrays.putIfAbsent(fullName, (RuntimeArray) runtimeValue) == null) {
installed = true;
}
} else if (runtimeValue instanceof RuntimeHash) {
GlobalVariable.globalHashes.put(fullName, (RuntimeHash) runtimeValue);
if (GlobalVariable.globalHashes.putIfAbsent(fullName, (RuntimeHash) runtimeValue) == null) {
installed = true;
}
} else if (runtimeValue instanceof RuntimeScalar) {
GlobalVariable.globalVariables.put(fullName, (RuntimeScalar) runtimeValue);
if (GlobalVariable.globalVariables.putIfAbsent(fullName, (RuntimeScalar) runtimeValue) == null) {
installed = true;
}
}
if (installed) {
evalAliasKeys.add(entry.name().charAt(0) + fullName);
}
evalAliasKeys.add(entry.name().charAt(0) + fullName);
}
}
}
Expand Down Expand Up @@ -1304,14 +1316,33 @@ public static RuntimeList evalStringWithInterpreter(
String varNameWithoutSigil = entry.name().substring(1);
String fullName = packageName + "::" + varNameWithoutSigil;

// Only install the alias (and schedule cleanup) if no other
// alias is already in place for this key. An alias may already
// exist because SubroutineParser.handleNamedSub registered one
// at compile time for a named sub that closes over the same
// outer `my`. That alias must stay alive until the owning
// `my %name = (...)` runs and calls retrieveBeginHash, which
// takes ownership. If we unconditionally put+remove here, we
// delete SubroutineParser's alias in the finally block, and the
// later `my` creates a fresh, unshared object — breaking the
// named sub's closure over that variable.
boolean installed = false;
if (runtimeValue instanceof RuntimeArray) {
GlobalVariable.globalArrays.put(fullName, (RuntimeArray) runtimeValue);
if (GlobalVariable.globalArrays.putIfAbsent(fullName, (RuntimeArray) runtimeValue) == null) {
installed = true;
}
} else if (runtimeValue instanceof RuntimeHash) {
GlobalVariable.globalHashes.put(fullName, (RuntimeHash) runtimeValue);
if (GlobalVariable.globalHashes.putIfAbsent(fullName, (RuntimeHash) runtimeValue) == null) {
installed = true;
}
} else if (runtimeValue instanceof RuntimeScalar) {
GlobalVariable.globalVariables.put(fullName, (RuntimeScalar) runtimeValue);
if (GlobalVariable.globalVariables.putIfAbsent(fullName, (RuntimeScalar) runtimeValue) == null) {
installed = true;
}
}
if (installed) {
evalAliasKeys.add(entry.name().charAt(0) + fullName);
}
evalAliasKeys.add(entry.name().charAt(0) + fullName);
}
}
}
Expand Down
25 changes: 14 additions & 11 deletions src/main/perl/lib/ExtUtils/MakeMaker.pm
Original file line number Diff line number Diff line change
Expand Up @@ -600,43 +600,46 @@ INSTALLSITELIB = $installsitelib
NOECHO = \@
RM_RF = rm -rf

all: pm_to_blib pure_all pl_files config
all:: pm_to_blib pure_all pl_files config
\t\@echo "PerlOnJava: $name v$version installed ($file_count files)"

# Copy module and data files to installation directory
pm_to_blib:
pm_to_blib::
$install_cmds_str

# Copy to blib/lib for test compatibility (make test uses PERL5LIB=./blib/lib)
# Also create blib/arch so that "use blib" / "-Mblib" works (blib.pm requires both)
pure_all:
pure_all::
\t\@mkdir -p blib/arch
$blib_cmds_str

# Process PL_FILES
pl_files:
pl_files::
$pl_cmds_str

# Install executable scripts
install_scripts:
install_scripts::
$script_cmds_str

# Use double-colon for config to allow postamble to add rules
# Use double-colon rules throughout so that postambles (e.g.
# Alien::Build's MY::postamble, File::ShareDir::Install) can add
# additional rules for the same target. Mixing : and :: is a fatal
# make error, and real ExtUtils::MakeMaker uses :: for these targets.
config::

test:
test::
\t$test_cmd

install: all install_scripts
install:: all install_scripts
\t\@echo "PerlOnJava: $name installed to \$(INSTALLSITELIB)"

clean:
clean::
\t\$(RM_RF) blib pm_to_blib

realclean: clean
realclean:: clean
\t\$(RM_RF) $makefile ${makefile}.old

distclean: clean
distclean:: clean
\t\$(RM_RF) $makefile ${makefile}.old

.PHONY: all pm_to_blib pure_all pl_files config test install clean realclean distclean install_scripts
Expand Down
60 changes: 60 additions & 0 deletions src/test/resources/unit/closure_eval_string.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use strict;
use warnings;
use Test::More;

# Regression test for a closure-capture bug exposed by
# Net::SSH::Perl's modulino Makefile.PL.
#
# When `sub do_it { ... %WM ... }` is a named (package) sub that
# closes over a file-scope `my %WM`, PerlOnJava implements the
# capture via a BEGIN-package alias registered by SubroutineParser
# at compile time. The alias must stay alive until the owning
# `my %WM = (...)` runs at runtime (at which point `retrieveBeginHash`
# takes ownership and removes the alias).
#
# The bug: `eval STRING` in a sibling sub would (a) re-install the
# same alias in the global hash at entry, and (b) unconditionally
# remove it in its `finally` block on exit, destroying the shared
# object. When `my %WM = (...)` then ran, it saw an empty global
# alias map and created a brand new RuntimeHash, disconnected from
# the one the named sub had already captured. Result: `sub do_it`
# saw an empty hash.
#
# This file must keep the `my` declarations at file scope (so named
# subs parsed below can legally close over them) and must call the
# `eval STRING`-containing sub BEFORE the `my` assignment runs.

# ---- Hash case ----
our @hash_order;
hash_foo(); # forward call, runs eval STRING
my %HASH = ( NAME => 'Foo', X => 'Y' ); # declared AFTER the call
hash_do();
sub hash_do { push @hash_order, join(",", sort keys %HASH); }
sub hash_foo { eval("1;"); push @hash_order, "foo"; }

is_deeply(\@hash_order, ["foo", "NAME,X"],
"named sub hash closure survives eval STRING in a sibling sub");

# ---- Array case ----
our @arr_order;
arr_foo();
my @ARR = (1, 2, 3);
arr_do();
sub arr_do { push @arr_order, join(",", @ARR); }
sub arr_foo { eval("1;"); push @arr_order, "foo"; }

is_deeply(\@arr_order, ["foo", "1,2,3"],
"named sub array closure survives eval STRING in a sibling sub");

# ---- Scalar case ----
our @sca_order;
sca_foo();
my $SCA = "hello";
sca_do();
sub sca_do { push @sca_order, defined($SCA) ? $SCA : "undef"; }
sub sca_foo { eval("1;"); push @sca_order, "foo"; }

is_deeply(\@sca_order, ["foo", "hello"],
"named sub scalar closure survives eval STRING in a sibling sub");

done_testing();
Loading