Skip to content
Open
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: 19 additions & 1 deletion dev/design/perl-ownership-rewrite.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,26 @@ and first path establish implementation cost.
target Perl loads the helper. These are pre-existing issues tracked in
[#1656](https://github.com/fglock/PerlOnJava/issues/1656) and
[#1434](https://github.com/fglock/PerlOnJava/issues/1434); the DBIx run's two
assertion failures remain separately unclassified under
assertion failures are tracked separately under
[#1646](https://github.com/fglock/PerlOnJava/issues/1646).
- [x] Classify DBIx::Class 0.082844 `t/storage/error.t` assertion 49 on the
current lifecycle candidate: system Perl passes, and the same failure
reproduces on the clean master baseline. A clean rebuild of the current
candidate still fails 48/49 assertions. The weak schema captured by DBI's
handler remains live after `$schema` is cleared; after a forced quiet GC,
owner tracing reports no active scalar owners or capture slots despite the
stale positive refcount. The exact stale-count/root cause remains open under
#1642; rerun after that lifecycle work is reconciled. This does not count as
DBIx acceptance.
- [x] Rerun the three DBIx lifecycle files together on the clean candidate.
`t/storage/txn_scope_guard.t` now passes 18/18 and `t/52leaks.t` passes 11/11
(including their TODO skips); the prior clean-master run failed the scope
guard assertion, and the earlier leak outcomes varied. `t/storage/error.t`
remains at 48/49, with assertion 49 failing as it does on clean master while
passing on system Perl. A reduced scope-guard reproducer passes on system
Perl, JVM, interpreter, and clean master, so it is not standalone regression
evidence. Full DBIx acceptance remains open until #1642 is reconciled and
the complete suite is rerun.

### Next steps

Expand Down
4 changes: 4 additions & 0 deletions docs/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ priorities and future plans.
- Snapshot mutable array entries while marking package roots and during reachability walks so concurrent thread teardown cannot fail with `ConcurrentModificationException`.

- Enforce restricted-hash key access and implement `Hash::Util` key, value, and hash locking (#1502, #1172).
- Preserve captured referent ownership when foreach lexicals alias aggregate cells, fixing prompt cleanup of nested one-shot callback owners (#1646).
- Match `B::REFCNT` to native B's treatment of its wrapper reference, preventing false resurrection from a schema's sole source registry owner (#1646).
- Re-fire `DESTROY` after a resurrected object's final captured owner is released, even when a full-root snapshot still includes the earlier caller frame (#1646).
- Recheck nested weak references after blessed container destruction returns, clearing DBIx::Class storage callbacks once their schema is gone (#1646).
- Report the caller's scalar context correctly when inspecting frames in overloaded exceptions (#1859).
- Avoid heap exhaustion when compiling very large generated subroutines, and preserve heredoc statement boundaries with `\&name` fallbacks (#1252).
- Cover nested require caller packages and imported `CLASS` constants, and match defer ordering and caller frames around loop `continue` blocks (#1601, #1405, #1383).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ private static void releaseConsumedTemp(RuntimeBase temp, RuntimeBase target) {
return;
}

tempScalar.refCountOwned = false;
tempScalar.setRefCountOwned(false);
if (base.refCountTrace) {
base.traceRefCount(-1, "BytecodeInterpreter.releaseConsumedTemp");
base.releaseOwner(tempScalar, "releaseConsumedTemp");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1355,7 +1355,7 @@ private static void releaseHashSliceTemporaryOwner(RuntimeScalar value) {
base.releaseOwner(value, "HASH_SLICE_SET temporary owner transfer");
base.releaseActiveOwner(value);
base.refCount--;
value.refCountOwned = false;
value.setRefCountOwned(false);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ private static RuntimeScalar bless(RuntimeScalar runtimeScalar, RuntimeScalar cl
// tracking wasn't active at assignment time). Count it as 1.
referent.refCount = 1;
referent.recordOwner(runtimeScalar, "rebless from untracked");
runtimeScalar.refCountOwned = true;
runtimeScalar.setRefCountOwned(true);
} else {
// First bless: start at refCount=1 and add to MortalList.
// The mortal entry will decrement back to 0 at the next
Expand Down Expand Up @@ -257,7 +257,7 @@ private static RuntimeScalar bless(RuntimeScalar runtimeScalar, RuntimeScalar cl
referent.refCount = existingScalarOwner ? 2 : 1;
if (existingScalarOwner) {
referent.recordOwner(runtimeScalar, "first bless of existing scalar ref");
runtimeScalar.refCountOwned = true;
runtimeScalar.setRefCountOwned(true);
}
referent.acquireTransientTraceOwner("bless mortal temporary",
"ReferenceOperators.first bless");
Expand Down
42 changes: 22 additions & 20 deletions src/main/java/org/perlonjava/runtime/perlmodule/Internals.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
* The Strict class provides functionalities similar to the Perl strict module.
*/
public class Internals extends PerlModuleBase {

/**
* Constructor initializes the module.
*/
Expand Down Expand Up @@ -543,24 +542,9 @@ public static RuntimeList svRefcount(RuntimeArray args, int ctx) {
// compensates for the ampersand-call argument alias below.
boolean compileHintsHash = base == GlobalVariable.getGlobalHash(
GlobalContext.encodeSpecialVar("H"));
boolean bObjectProbe = args.size() > 1 && args.get(1).getBoolean();
int extra = (base.localBindingExists && !compileHintsHash ? 1 : 0)
+ base.foreachAliasCount;
if (rc == 2
&& args.size() > 1
&& args.get(1).getBoolean()
&& DestroyDispatch.isInsideDestroy()
&& !ReachabilityWalker.hasLiveStrongScalarReferentOtherThan(base, arg)) {
// B::SV's private hash slot is one of the two selective
// owners. Ordinarily it is the temporary owner discounted
// below (a single live lexical therefore reports one). If
// there is no independently live scalar pad, the other owner
// is a real aggregate slot, as in DBIx::Class Schema's source
// registry during DESTROY, and must remain visible to B. This
// distinction is only meaningful while DESTROY is active:
// outside it, an unregistered lexical can look identical and
// must still report its single owner.
extra++;
}
// Legacy fudge: anonymous tracked container with no counted
// owners -- still report 1 to indicate "live SV". Used by
// Sub::Quote / Moo introspection paths that probe for liveness.
Expand All @@ -576,8 +560,8 @@ public static RuntimeList svRefcount(RuntimeArray args, int ctx) {
boolean fieldOwnedMethodResult = rc == 1
&& !base.localBindingExists
&& base.hashSlotOwnerCount > 0
&& !ReachabilityWalker.hasLiveStrongScalarReferentOtherThan(base, arg);
boolean bObjectProbe = args.size() > 1 && args.get(1).getBoolean();
&& (bObjectProbe
|| !ReachabilityWalker.hasLiveStrongScalarReferentOtherThan(base, arg));
int adjust = compileHintsHash
? -1
: base.localBindingExists || fieldOwnedMethodResult ? 0 : -1;
Expand All @@ -589,7 +573,17 @@ public static RuntimeList svRefcount(RuntimeArray args, int ctx) {
if (bObjectProbe && base.localBindingExists && !compileHintsHash) {
adjust = -1;
}
return new RuntimeScalar(rc + extra + adjust).getList();
boolean insideDestroy = DestroyDispatch.isInsideDestroy();
int staleHashOwners = 0;
if (rc > 1 && bObjectProbe && !base.localBindingExists && insideDestroy) {
int[] staleOwners = base.unreachableDirectHashSlotOwnerCounts();
// Once a counted owner has leaked, include every unreachable
// hash slot in the correction. Some slots retain the raw
// refcount after their wrapper has already cleared its flag.
if (staleOwners[0] > 0) staleHashOwners = staleOwners[1];
}
int reported = Math.max(0, rc + extra + adjust - staleHashOwners);
return new RuntimeScalar(reported).getList();
}
return new RuntimeScalar(1).getList();
}
Expand All @@ -613,12 +607,16 @@ public static RuntimeList jperlReferenceByAddress(RuntimeArray args, int ctx) {
* <li>{@code refCount} — raw {@link RuntimeBase#refCount}</li>
* <li>{@code localBindingExists} — true when a named-variable slot still holds the container</li>
* <li>{@code destroyFired} — true once DESTROY has run</li>
* <li>{@code needs_re_destroy} — true when a later release should re-fire DESTROY</li>
* <li>{@code currently_destroying} — true while the Perl DESTROY body is active</li>
* <li>{@code perl_lifecycle_state} — current explicit destruction lifecycle state</li>
* <li>{@code blessId} — bless class id (0 = unblessed)</li>
* <li>{@code class_name} — Perl class name (empty string if unblessed)</li>
* <li>{@code kind} — runtime type: SCALAR / ARRAY / HASH / CODE / GLOB / OTHER</li>
* <li>{@code has_weak_refs} — true if the weak-ref registry has entries pointing here</li>
* <li>{@code active_owner_count} — live scalar-store owners currently tracked for diagnostics</li>
* <li>{@code semantic_capture_owner_count} — distinct captured pad owners</li>
* <li>{@code hash_slot_owner_count} — direct strong owners in hash slots</li>
* </ul>
*/
public static RuntimeList jperl_refstate(RuntimeArray args, int ctx) {
Expand All @@ -628,11 +626,15 @@ public static RuntimeList jperl_refstate(RuntimeArray args, int ctx) {
result.put("refCount", new RuntimeScalar(base.refCount));
result.put("localBindingExists", new RuntimeScalar(base.localBindingExists));
result.put("destroyFired", new RuntimeScalar(base.destroyFired));
result.put("needs_re_destroy", new RuntimeScalar(base.needsReDestroy));
result.put("currently_destroying", new RuntimeScalar(base.currentlyDestroying));
result.put("perl_lifecycle_state", new RuntimeScalar(base.perlLifecycleState().name()));
result.put("blessId", new RuntimeScalar(base.blessId));
String className = NameNormalizer.getBlessStr(base.blessId);
result.put("class_name", new RuntimeScalar(className == null ? "" : className));
result.put("active_owner_count", new RuntimeScalar(base.activeOwnerCount()));
result.put("semantic_capture_owner_count", new RuntimeScalar(base.semanticCaptureOwnerCount()));
result.put("hash_slot_owner_count", new RuntimeScalar(base.hashSlotOwnerCount));
String kind = "OTHER";
if (base instanceof RuntimeGlob) kind = "GLOB";
else if (base instanceof RuntimeHash) kind = "HASH";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ public static void releaseApplyArgs(RuntimeArray args) {
base.releaseOwner(elem, "Storable.releaseApplyArgs");
base.releaseActiveOwner(elem);
base.refCount--;
elem.refCountOwned = false;
elem.setRefCountOwned(false);
}
}
args.elements.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ public static void vivify(RuntimeArray array) {

RuntimeScalar referent = arrayProxy.scalarToAutovivify;
if (referent instanceof RuntimeBaseProxy proxy) {
proxy.set(array.createReference());
// A proxy is an aggregate element slot, not the local pad
// binding for a named @array. Marking this array as
// localBindingExists would keep it alive after its hash/array
// slot is replaced, so captured callbacks in the old array
// would never be released.
proxy.set(array.createAnonymousReference());
} else {
referent.value = array;
referent.type = RuntimeScalarType.ARRAYREFERENCE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ private static void doCallDestroy(RuntimeBase referent, String className) {
}
base.releaseActiveOwner(elem);
base.refCount--;
elem.refCountOwned = false;
elem.setRefCountOwned(false);
}
}
args.elements.clear();
Expand Down Expand Up @@ -635,6 +635,16 @@ private static void doCallDestroy(RuntimeBase referent, String className) {
RuntimeScalar.scopeExitCleanup(scalar);
MortalList.flush();
}
// A blessed container may hold weakly observed objects whose
// selective counts remain inflated after its fields are released.
// Reconsider those descendants after DESTROY has returned, when
// its temporary $self lexical is no longer a Perl root. DBIx::Class
// keeps a weak Storage reference in the DBI error callback and
// expects it to be cleared as soon as the owning Schema is gone.
if ((referent instanceof RuntimeHash || referent instanceof RuntimeArray)
&& MortalList.requestWeakSweepsForDestroyedContainer(referent)) {
requestSweepAfterOuterDestroy();
}
} catch (Exception e) {
String msg = e.getMessage();
if (msg == null) msg = e.getClass().getName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ public void dynamicRestoreState() {
&& localVar.value instanceof RuntimeBase base
&& base.refCount > 0) {
displacedBase = base;
localVar.refCountOwned = false;
localVar.setRefCountOwned(false);
displacedBase.releaseActiveOwner(localVar);
}
if (localVar != null
Expand Down Expand Up @@ -281,7 +281,7 @@ public void dynamicRestoreState() {
localVar.value = saved.originalVariable.value;
localVar.blessId = saved.originalVariable.blessId;
}
localVar.refCountOwned = false;
localVar.setRefCountOwned(false);
}
if (localVar != null) {
localVar.ownsScalarReferenceContents = false;
Expand Down
Loading
Loading