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
20 changes: 17 additions & 3 deletions dev/architecture/weaken-destroy.md
Original file line number Diff line number Diff line change
@@ -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`
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/org/perlonjava/runtime/io/LayeredIOHandle.java
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<String, String> inputTransform = s -> layer.processInput(s);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))) {
Expand Down
19 changes: 19 additions & 0 deletions src/test/resources/unit/perlio_encoding_loads_encode.t
Original file line number Diff line number Diff line change
@@ -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;
Loading