diff --git a/dev/design/issue-queue.md b/dev/design/issue-queue.md index 4dcf6b2c55..376bc326b8 100644 --- a/dev/design/issue-queue.md +++ b/dev/design/issue-queue.md @@ -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. | diff --git a/docs/about/changelog.md b/docs/about/changelog.md index cec5e2dd13..7baf17704b 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -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). diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index a5fdf033ad..035f6628ac 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -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 diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index d45c22c80e..f3f1a0c959 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -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(); } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStash.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStash.java index b6733bf066..fae11442c4 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStash.java @@ -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; } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java index 8ddfb8be8b..1563ae6cf1 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java @@ -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 @@ -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(); @@ -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()); diff --git a/src/test/resources/unit/stash_each_typeglob_copy.t b/src/test/resources/unit/stash_each_typeglob_copy.t new file mode 100644 index 0000000000..fe3a9e1825 --- /dev/null +++ b/src/test/resources/unit/stash_each_typeglob_copy.t @@ -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;