From 06d4f1d1208d4e889ca92fd8b3d2c87f8712a59b Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 24 Jul 2026 11:38:27 +0200 Subject: [PATCH 1/2] fix: load Encode for PerlIO encoding layers Match Perl's visible side effect of loading Encode when an :encoding(...) layer is applied. Pod::Spell relies on this after its wordlist is opened as UTF-8. Add a focused regression test for Encode::encode availability. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex --- .../runtime/io/LayeredIOHandle.java | 6 ++++++ .../unit/perlio_encoding_loads_encode.t | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 src/test/resources/unit/perlio_encoding_loads_encode.t diff --git a/src/main/java/org/perlonjava/runtime/io/LayeredIOHandle.java b/src/main/java/org/perlonjava/runtime/io/LayeredIOHandle.java index c706e0467..9652c1cb7 100644 --- a/src/main/java/org/perlonjava/runtime/io/LayeredIOHandle.java +++ b/src/main/java/org/perlonjava/runtime/io/LayeredIOHandle.java @@ -1,5 +1,6 @@ package org.perlonjava.runtime.io; +import org.perlonjava.runtime.operators.ModuleOperators; import org.perlonjava.runtime.runtimetypes.PerlJavaUnimplementedException; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; @@ -453,6 +454,11 @@ private void addLayer(String layerSpec) { String charsetName = layerSpec.substring(9, layerSpec.length() - 1); try { Charset charset = resolveEncodingCharset(charsetName); + // Perl's :encoding(...) layer is provided by PerlIO::encoding + // and loads Encode as a visible side effect. Some CPAN modules + // (including Pod::Spell) rely on Encode::* being available + // after an encoded handle has been opened. + ModuleOperators.require(new RuntimeScalar("Encode.pm")); EncodingLayer layer = new EncodingLayer(charset, layerSpec); activeLayers.add(layer); Function inputTransform = s -> layer.processInput(s); diff --git a/src/test/resources/unit/perlio_encoding_loads_encode.t b/src/test/resources/unit/perlio_encoding_loads_encode.t new file mode 100644 index 000000000..10fde6a8b --- /dev/null +++ b/src/test/resources/unit/perlio_encoding_loads_encode.t @@ -0,0 +1,19 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; +use File::Temp qw(tempfile); + +my ($raw_fh, $filename) = tempfile(); +close $raw_fh; + +open my $encoded_fh, '>:encoding(UTF-8)', $filename + or die "open with encoding layer: $!"; + +ok(defined(&Encode::encode), + ':encoding(...) loads Encode functions'); + +close $encoded_fh; +unlink $filename; + +done_testing; From 236ffb21d69a446bed4aa02f9cd381e2c26b8ebb Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 24 Jul 2026 13:31:02 +0200 Subject: [PATCH 2/2] fix: clear weak refs to scalar return temporaries Birth-track unbound scalar referents that have never occupied a lexical, closure, package root, or aggregate slot. This lets weakening the sole reference consume it immediately without a reachability walk. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex --- dev/architecture/weaken-destroy.md | 20 +++++++++++++--- .../runtime/runtimetypes/RuntimeScalar.java | 24 ++++++++++++++++++- .../runtime/runtimetypes/WeakRefRegistry.java | 4 +++- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/dev/architecture/weaken-destroy.md b/dev/architecture/weaken-destroy.md index a81c6065b..f5e09d6df 100644 --- a/dev/architecture/weaken-destroy.md +++ b/dev/architecture/weaken-destroy.md @@ -1,6 +1,6 @@ # Weaken & DESTROY - Architecture Guide -**Last Updated:** 2026-04-30 +**Last Updated:** 2026-07-24 **Status:** PRODUCTION READY - 841/841 Moo subtests (100%) - 13858/13858 DBIx::Class subtests across 314 test files (100%, 0 Dubious) — measured on branch `perf/dbic-safe-port` at `2ef41907d` @@ -135,6 +135,18 @@ back-references) and cascade to break Moo's accessor inlining. | `-2` | **WEAKLY_TRACKED.** Entered when `weaken()` is called on an untracked non-CODE object (refCount == -1). A heuristic allowing weak ref clearing when a strong ref is explicitly dropped via `undefine()`. `setLarge()` and `scopeExitCleanup()` do NOT clear weak refs for this state — only explicit `undefine()`. | | `MIN_VALUE` | **Destroyed.** DESTROY has been called (or is in progress). Prevents double-destruction. | +### Scalar-reference temporaries + +`RuntimeScalar.createReference()` birth-tracks an unbound scalar return value +when it has no lexical registration, closure capture, package-global root, or +aggregate owner. This makes `weaken(\(sub_return()))` clear immediately, as in +Perl, while references to named scalars and aggregate elements remain alive. + +The promoted scalar is marked `ephemeralScalarReferenceReferent`. When its sole +counted reference is weakened, `WeakRefRegistry` can clear it directly without +calling `ReachabilityWalker`: the ownership checks performed at promotion prove +there is no hidden strong owner. This keeps the hot operation constant-time. + ### Ownership: `refCountOwned` Each `RuntimeScalar` has a `boolean refCountOwned` field. When true, this scalar @@ -966,8 +978,10 @@ decrement per reference assignment), but this is by design. - **Per-referent:** `refCount` (int, 4 bytes) and `blessId` (int, 4 bytes) on `RuntimeBase`. Always present but unused when untracked. -- **Per-scalar:** `refCountOwned` (boolean, 1 byte) and `captureCount` (int, - 4 bytes) on `RuntimeScalar`. Always present. +- **Per-scalar:** `refCountOwned` and + `ephemeralScalarReferenceReferent` (booleans), plus `captureCount` (int, + 4 bytes), on `RuntimeScalar`. Always present; the JVM may pack the booleans + into existing object-alignment padding. - **WeakRefRegistry:** External identity maps. Only allocated when `weaken()` is called. Zero memory when no weak refs exist. - **DestroyDispatch caches:** `BitSet` + `ConcurrentHashMap`. Negligible. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index 4e1e65468..ab3493d69 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -281,6 +281,14 @@ void retainClosureCaptureReferentsForUnweaken() { */ public boolean referencedByScalarReference; + /** + * True when {@link #createReference()} promoted an otherwise-unbound + * scalar temporary into selective reference counting. Such a referent has + * no hidden lexical, closure, global, or aggregate owner, so weakening its + * sole reference can clear it without a reachability walk. + */ + boolean ephemeralScalarReferenceReferent; + // Constructors public RuntimeScalar() { this.type = UNDEF; @@ -2780,11 +2788,25 @@ public RuntimeScalar codeDerefNonStrict(String packageName) { // Internals::SvREADONLY needs the container to set/get readonly status. public RuntimeScalar createReference() { referencedByScalarReference = true; + boolean isRegisteredLexical = + this.refCount == -1 && MyVarCleanupStack.isRegistered(this); if (this.refCount == -1 - && MyVarCleanupStack.isRegistered(this) + && isRegisteredLexical && !RuntimeCode.hasActiveCode()) { this.refCount = 0; this.localBindingExists = true; + } else if (this.refCount == -1 + && !isRegisteredLexical + && captureCount == 0 + && !isPackageGlobalRoot + && containerOwner == null) { + // An unbound scalar value returned from a subroutine is an + // ephemeral Perl SV. Birth-track it so weaken(\(sub_return())) + // consumes its sole owner immediately. Bound lexicals, closure + // captures, globals, aggregate elements, and active @_ arguments + // retain the established untracked/WEAKLY_TRACKED behavior. + this.refCount = 0; + this.ephemeralScalarReferenceReferent = true; } RuntimeScalar result = new RuntimeScalar(); result.type = RuntimeScalarType.REFERENCE; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/WeakRefRegistry.java b/src/main/java/org/perlonjava/runtime/runtimetypes/WeakRefRegistry.java index a19ee4c85..a6c875a8b 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/WeakRefRegistry.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/WeakRefRegistry.java @@ -141,7 +141,9 @@ public static void weaken(RuntimeScalar ref) { // slot holds a strong reference not counted in refCount. // Don't call callDestroy — the container is still alive. // Cleanup will happen at scope exit (scopeExitCleanupHash/Array). - } else if (hasWeakRefsTo(base) + } else if (!(base instanceof RuntimeScalar scalar + && scalar.ephemeralScalarReferenceReferent) + && hasWeakRefsTo(base) && (ReachabilityWalker.isReachableFromRoots(base) || ReachabilityWalker.isReachableFromLiveScalarRegistry(base) || ReachabilityWalker.isReachableFromLiveCodeCaptures(base))) {