Skip to content

Commit 3bd0160

Browse files
fix(D-W6): restore walker gate and document drift-source findings
Empirical D-W6 progress is documented in moose_support.md. Findings from the no-gate experiment (commit 230a672): PASS — what removing the gate fixes (Perl 5 semantics replicated): - t/cdbi/04-lazy.t test 11 (no phantom destroy clobbers row build). - t/storage/txn_scope_guard.t test 18 (double-DESTROY warning fires). - t/52leaks.t 11/11 (cycles leak naturally, $r stays defined). - Refcount unit tests still pass. FAIL — what removing the gate breaks (refCount drift in MOP code): - use Class::MOP::Class fails: "Can't locate method initialize" — the CV for Class::MOP::Class::initialize is destroyed during the circular bootstrap. PJ_DESTROY_TRACE shows non-blessed RuntimeCode objects hitting refCount=0 inside MortalList.flush during MiniTrait::apply, which is a sub-installation path. - use Moose itself works (different load order) but cmop bootstrap destroys subs prematurely. Hybrid attempt (only destroy blessed objects through the gate path) fixed the bootstrap but regressed t/52leaks.t cascade-cleanup of non-blessed containers holding blessed children. Conclusion: the proper D-W6 fix is multi-week — it requires auditing each cooperative refCount source (sub installation, glob assignment, @_ promotion, hash-slot stores, closure captures) and back-filling the missing increments. Until then, restore the universal walker gate as a safety net (no class-name dispatch; matches PR #599). Adds a PJ_DESTROY_TRACE=1 env-flag in DestroyDispatch.callDestroy to help future drift hunting. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent fa76ea6 commit 3bd0160

3 files changed

Lines changed: 51 additions & 15 deletions

File tree

src/main/java/org/perlonjava/runtime/runtimetypes/DestroyDispatch.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
*/
1818
public class DestroyDispatch {
1919

20+
/** Phase D-W6 debug: enable destroy tracing via -Dperlonjava.destroyTrace=1
21+
* or env PJ_DESTROY_TRACE=1. */
22+
private static final boolean DESTROY_TRACE =
23+
"1".equals(System.getProperty("perlonjava.destroyTrace"))
24+
|| "1".equals(System.getenv("PJ_DESTROY_TRACE"));
25+
2026
// BitSet indexed by |blessId| — set if the class defines DESTROY (or AUTOLOAD)
2127
private static final BitSet destroyClasses = new BitSet();
2228

@@ -108,6 +114,19 @@ public static void invalidateCache() {
108114
public static void callDestroy(RuntimeBase referent) {
109115
// refCount is already MIN_VALUE (set by caller)
110116

117+
// Phase D-W6 debug: optional trace of every destroy call.
118+
// Enable with -Dperlonjava.destroyTrace=1 (or env PJ_DESTROY_TRACE=1)
119+
// to find refCount-drift sources.
120+
if (DESTROY_TRACE) {
121+
String klass = referent.blessId != 0
122+
? NameNormalizer.getBlessStr(referent.blessId)
123+
: referent.getClass().getSimpleName();
124+
System.err.println("[DESTROY] " + klass + "@"
125+
+ System.identityHashCode(referent)
126+
+ " refCount=" + referent.refCount);
127+
new RuntimeException("destroy trace").printStackTrace(System.err);
128+
}
129+
111130
// Phase 3 (refcount_alignment_plan.md): Re-entry guard.
112131
// If this object is already inside its own DESTROY body, a transient
113132
// decrement-to-0 (local temp release, deferred MortalList flush,

src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -555,18 +555,34 @@ public static void flush() {
555555
// leak-tracing scenarios; those scenarios now use
556556
// createAnonymousReference() (localBindingExists stays false)
557557
// so the clear is no longer needed and broke #76716.
558+
} else if (base.blessId != 0
559+
&& WeakRefRegistry.hasWeakRefsTo(base)
560+
&& ReachabilityWalker.isReachableFromRoots(base)) {
561+
// Phase D-W6 (deferred): walker gate retained as
562+
// a temporary safeguard until cooperative
563+
// refCount accuracy is audited and fixed at the
564+
// sources documented in
565+
// dev/modules/moose_support.md (Phase D-W6).
566+
//
567+
// Empirically: removing the gate makes cycles
568+
// leak correctly (52leaks.t passes), DESTROY
569+
// fires twice on the second-DESTROY pattern
570+
// (txn_scope_guard.t passes), and DBIC row
571+
// construction stops losing column data
572+
// (cdbi/04-lazy.t passes). But it also breaks
573+
// Moose bootstrap because anonymous CVs are
574+
// briefly the only ref to themselves before
575+
// being installed in package stashes; the
576+
// cooperative refCount transient-drops to 0,
577+
// DESTROY fires on a CV that should still be
578+
// live, and Class::MOP::Class loses its
579+
// `initialize` method.
580+
//
581+
// The walker check absorbs this drift safely.
582+
// It is no longer class-name-based (PR #599)
583+
// and uses MyVarCleanupStack-seeded reachability
584+
// so live `my` variables pin their referents.
558585
} else {
559-
// Phase D-W6: cooperative refCount is the single
560-
// source of truth for DESTROY firing — no walker
561-
// gate. Matches Perl 5 semantics:
562-
// - Cycles leak (cooperative refCount keeps
563-
// cycle members at refCount ≥ 1).
564-
// - DESTROY fires at every refCount=0
565-
// transition (no deferral via reachability
566-
// analysis).
567-
// Any drift in cooperative refCount must be
568-
// fixed at the source, not papered over here.
569-
// See dev/modules/moose_support.md (D-W6).
570586
base.refCount = Integer.MIN_VALUE;
571587
DestroyDispatch.callDestroy(base);
572588
}

src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,11 +1195,12 @@ private RuntimeScalar setLargeRefCounted(RuntimeScalar value) {
11951195
// slot holds a strong reference not counted in refCount.
11961196
// Don't call callDestroy — the container is still alive.
11971197
// Cleanup will happen at scope exit (scopeExitCleanupHash/Array).
1198+
} else if (oldBase.blessId != 0
1199+
&& WeakRefRegistry.hasWeakRefsTo(oldBase)
1200+
&& ReachabilityWalker.isReachableFromRoots(oldBase)) {
1201+
// Phase D-W6 (deferred): see MortalList.flush() for
1202+
// rationale — gate retained until refCount audit.
11981203
} else {
1199-
// Phase D-W6: cooperative refCount is the single
1200-
// source of truth — no walker gate. See
1201-
// MortalList.flush() and dev/modules/moose_support.md
1202-
// (D-W6) for the rationale.
12031204
oldBase.refCount = Integer.MIN_VALUE;
12041205
DestroyDispatch.callDestroy(oldBase);
12051206
}

0 commit comments

Comments
 (0)