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
1 change: 1 addition & 0 deletions dev/design/issue-queue.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ resolved before implementation. No new pass or failure is asserted here.
| Dynamic class method lookup | [#1959](https://github.com/fglock/PerlOnJava/issues/1959) Object::Hybrid `tieable` promotion | Normal / Active | Generated `HASH_STATIC` declares a bodyless `can`; route it through that class's `AUTOLOAD` when present. Focused reproducer and full `make` pass on both backends. The full upstream suite advances beyond this failure but later exits at `IO::File::FILENO` (separate behavior). Candidate fix is in [PR #1960](https://github.com/fglock/PerlOnJava/pull/1960). |
| Scalar introspection | [#1954](https://github.com/fglock/PerlOnJava/issues/1954) `Scalar::Util::isdual` booleans | Normal / Active | Mars 0.05 passes on system Perl and failed assertions 10–11 on both PerlOnJava backends (run `20261103-123832-91742`). [Draft PR #1956](https://github.com/fglock/PerlOnJava/pull/1956) recognizes BOOLEAN values; focused boolean, explicit-dualvar and plain-string coverage plus all 13 Mars assertions pass on both backends, and full `make` passes. |
| POSIX filehandles | [#1932](https://github.com/fglock/PerlOnJava/issues/1932) `POSIX::isatty` glob handling | Normal / Active | IO-Pty-Easy 0.10 passes on system Perl; CPAN run `20261030-095654-34969` failed on both PerlOnJava backends because `*STDIN` reached a direct `RuntimeIO` cast. The helper-based fix and focused test pass on both backends; full `make` passes on `fix/posix-isatty-glob`. A fresh `jcpan -t IO::Pty::Easy` rerun timed out after `t/open-close.t` passed and fork-dependent tests skipped. |
| Stash iteration | [#1963](https://github.com/fglock/PerlOnJava/issues/1963) CODE globs copied from `each` | Normal / Active | DDC::Concordance 0.50 failed on both backends when `DDC::Any::mapstash` copied module-created closures from `each %stash`; system Perl passed. The slot snapshot/alias fix in [PR #1966](https://github.com/fglock/PerlOnJava/pull/1966) and eval-based regression pass both backends; all 471 DDC tests pass on the JVM backend, and the five affected files pass on the interpreter. |
| File-handle EOF state | [#1922](https://github.com/fglock/PerlOnJava/issues/1922) `IO::Handle::clearerr` | Normal / Active | `Log::Procmail` 0.14 passes on system Perl but failed on both PerlOnJava backends when reading appended data after EOF. The shared runtime fix and permanent regression are in [draft PR #1924](https://github.com/fglock/PerlOnJava/pull/1924); the full make gate and all 281 CPAN tests pass. Await review and CI. |
| File metadata | [#1912](https://github.com/fglock/PerlOnJava/issues/1912) `Time::HiRes::stat` | Normal / Active | Daiku 1.004 passes on system Perl and exposed the missing runtime function; the implementation and both-backend regression are in [PR #1913](https://github.com/fglock/PerlOnJava/pull/1913). |
| XML provider APIs | [#1892](https://github.com/fglock/PerlOnJava/issues/1892) XSLT output | High / Active | [PR #1894](https://github.com/fglock/PerlOnJava/pull/1894) is open; verify its serialization fix against HTML::ToDocBook. Coordinate through that work rather than start a duplicate. |
Expand Down
2 changes: 2 additions & 0 deletions docs/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ priorities and future plans.

## Work in progress

- Preserve callable CODE slots and captured values when copying stash globs returned by `each`.

- Autovivify undefined hash references assigned through a hash dereference (#1958).

- Dispatch bodyless `can` stubs through their class `AUTOLOAD` when present, preserving generated class introspection (#1959).
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,35 @@ private static boolean isCompiledDeclarationInstall(RuntimeCode oldCode, Runtime
&& newCode.deparseSourceText != null;
}

/** Install an existing CODE scalar as a typeglob-copy alias. */
public static void aliasCodeSlot(String globName, RuntimeScalar sourceCodeRef) {
if (globName == null || sourceCodeRef == null
|| sourceCodeRef.type != CODE
|| !(sourceCodeRef.value instanceof RuntimeCode newCode)) {
throw new IllegalArgumentException("A named CODE reference is required");
}
RuntimeScalar oldCodeRef = GlobalVariable.globalCodeRefs.get(globName);
if (oldCodeRef == sourceCodeRef) {
return;
}
if (oldCodeRef != null && oldCodeRef.value instanceof RuntimeCode oldCode
&& oldCode.stashRefCount > 0) {
oldCode.stashRefCount--;
}
GlobalVariable.globalCodeRefs.put(globName, sourceCodeRef);
GlobalVariable.replacePinnedCodeRef(globName, sourceCodeRef);
newCode.hadStashRef = true;
newCode.stashRefCount++;
GlobalVariable.isSubs.put(globName, true);
GlobalVariable.invalidatePackageRootSnapshot();
InheritanceResolver.invalidateCache();
int lastColonIdx = globName.lastIndexOf("::");
if (lastColonIdx > 0) {
org.perlonjava.runtime.perlmodule.Mro.incrementPackageGeneration(
globName.substring(0, lastColonIdx));
}
}

protected static boolean fillForwardCodeRefInPlace(String globName, RuntimeScalar codeContainer, RuntimeScalar value) {
if (codeContainer == null
|| codeContainer.type != CODE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,7 @@ public RuntimeScalar(RuntimeGlob value) {
// Skip detached copy for anonymous globs (null globName) since they
// don't participate in local/global scope and we need to preserve
// their local slots (e.g., from stash delete).
if (value.globName != null) {
if (value.globName != null && !value.isSlotSnapshot()) {
value = value.createDetachedCopy();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -730,7 +730,17 @@ public RuntimeScalar next() {
return new RuntimeScalar(currentEntry.getKey());
} else {
returnKey = true;
return currentEntry.getValue();
RuntimeScalar value = currentEntry.getValue();
if (value.value instanceof RuntimeStashEntry entry) {
return new RuntimeScalar(entry.createStashIteratorCopy());
}
if (value.value instanceof RuntimeGlob glob
&& glob.globName != null && !glob.isSlotSnapshot()) {
RuntimeGlob copy = glob.createDetachedCopy();
copy.setStringificationName(glob.globName);
return new RuntimeScalar(copy);
}
return value;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ public RuntimeGlob createDetachedCopy() {
return this;
}

/** Snapshot this named stash glob for a value returned by each(). */
public RuntimeGlob createStashIteratorCopy() {
RuntimeGlob copy = super.createDetachedCopy();
copy.setStringificationName(this.globName);
// Preserve the canonical CODE scalar identity: a glob copied through
// each() aliases the same CV slot, even though its other slots are a
// snapshot of the source glob.
copy.codeSlot = GlobalVariable.globalCodeRefs.get(this.globName);
return copy;
}

/**
* A constant.pm proxy is a scalar-reference value stored directly in the
* stash hash, not a typeglob value. Taking a reference to that hash value
Expand Down Expand Up @@ -294,20 +305,18 @@ public RuntimeScalar set(RuntimeScalar value) {
GlobalVariable.globalGlobs.put(this.globName, true);
GlobalVariable.markExplicitGlobAssignment(this.globName);

if (sourceGlobName == null) {
// Detached glob (e.g. returned by delete $stash->{name}):
// its slot values live on the glob object itself, not in
// GlobalVariable maps. Re-install those slots into the
// destination's slot-indexed maps directly (do NOT route
// through this.set(codeSlot), since that can self-assign
// the pinned CODE ref back onto itself via
// defineGlobalCodeRef -> getGlobalCodeRef returning the
// very same RuntimeScalar, which breaks refCount
// accounting for reference-type slots).
//
// This supports the common round-trip pattern:
// my $saved = delete $stash->{name};
// $stash->{name} = $saved;
if (sourceGlob.isSlotSnapshot()) {
// A typeglob snapshot preserves the source GV identity
// and slots. Use normal whole-glob aliasing for its other
// slots, then retain the exact captured CODE scalar.
super.set(sourceGlob);
RuntimeScalar sourceCode = sourceGlob.getSavedCodeSlot();
if (sourceCode != null && sourceCode.type == CODE
&& sourceCode.value instanceof RuntimeCode) {
RuntimeGlob.aliasCodeSlot(this.globName, sourceCode);
}
} else if (sourceGlobName == null) {
// Detached glob values keep their captured slots locally.
if (sourceGlob.codeSlot != null) {
GlobalVariable.globalCodeRefs.put(this.globName, sourceGlob.codeSlot);
InheritanceResolver.invalidateCache();
Expand All @@ -325,7 +334,7 @@ public RuntimeScalar set(RuntimeScalar value) {
GlobalVariable.scalarSlots().put(this.globName, sourceGlob.scalarSlot);
}
} else {
// Copy all slots from source to destination
// Live named glob values keep resolving through the source name.
this.set(GlobalVariable.getGlobalCodeRef(sourceGlobName));
this.set(GlobalVariable.getGlobalIO(sourceGlobName));
this.set(GlobalVariable.getGlobalArray(sourceGlobName).createReference());
Expand Down
46 changes: 46 additions & 0 deletions src/test/resources/unit/stash_each_typeglob_copy.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use strict;
use warnings;
use Test::More;

my $source_loaded = eval q{
package Local::StashEachCopy::Source;
our @HitSortEnum = qw(NoSort RandomSort LessByDate);
our %HitSortEnum = (NoSort => 0, RandomSort => 15, LessByDate => 1);

foreach my $ename (@HitSortEnum) {
no strict 'refs';
*$ename = sub { return $HitSortEnum{$ename}; };
}
1;
};
die $@ unless $source_loaded;

{
no strict 'refs';
my $source = \%{'Local::StashEachCopy::Source::'};
my $destination = \%{'Local::StashEachCopy::Destination::'};
while (my ($name, $glob) = each %$source) {
$destination->{$name} = $glob;
}
}

my %expected = (NoSort => 0, RandomSort => 15, LessByDate => 1);
for my $name (qw(NoSort RandomSort LessByDate)) {
is(
Local::StashEachCopy::Source->can($name)->(),
$expected{$name},
"$name source closure retains its captured enum value",
);
is(
Local::StashEachCopy::Destination->can($name)->(),
$expected{$name},
"$name remains callable after copying stash entries returned by each",
);
is(
Local::StashEachCopy::Destination->can($name),
Local::StashEachCopy::Source->can($name),
"$name keeps its coderef identity after copying stash entries returned by each",
);
}

done_testing;
Loading