Skip to content

WIP: DESTROY support for blessed objects#450

Closed
fglock wants to merge 1 commit into
masterfrom
feature/destroy-blessed-objects
Closed

WIP: DESTROY support for blessed objects#450
fglock wants to merge 1 commit into
masterfrom
feature/destroy-blessed-objects

Conversation

@fglock
Copy link
Copy Markdown
Owner

@fglock fglock commented Apr 7, 2026

Summary

Add DESTROY support for blessed objects. When a blessed reference is discarded, its DESTROY method is called if one exists in the class hierarchy.

Triggers

  • undef $obj - explicitly discards a blessed reference
  • delete $hash{key} - removes a blessed reference from a hash
  • Loop scope exit - existing scopeExitCleanup path now also fires DESTROY

Implementation

  • RuntimeBase.java: Added destroyCalled boolean flag to prevent double-DESTROY
  • RuntimeScalar.java: Added callDestroyIfNeeded() static method that finds DESTROY via InheritanceResolver.findMethodInHierarchy(), calls it in void context, and catches exceptions as "(in cleanup)" warnings
  • RuntimeHash.java: Calls callDestroyIfNeeded() on values removed by delete()
  • EmitStatement.java: Updated comments to reflect DESTROY in scope-exit cleanup

Limitations

Without reference counting, DESTROY may fire early if other references to the object still exist. The destroyCalled flag prevents double-DESTROY but does not solve the fundamental problem.

Scope-exit DESTROY is currently limited to loop bodies only. Extending it to all scopes (subroutines, eval blocks, if/else) requires reference counting to avoid destroying objects that are still referenced elsewhere. This was tested and caused 20+ unit test failures.

Future work

Targeted reference counting on blessed objects would allow safe scope-exit DESTROY for all contexts. Key points:

  • Add refCount field to RuntimeBase, increment on copy/bless, decrement on overwrite/scope-exit
  • Only call DESTROY when refCount reaches 0
  • Zero impact on hot path (reference types always go through setLarge())

Test plan

  • make passes
  • Reference counting implementation (future)
  • Scope-exit DESTROY for all contexts (future)

Generated with Devin

Call DESTROY on blessed objects when:
- delete $hash{key} removes a blessed reference (RuntimeHash)
- undef $var explicitly discards a blessed reference (RuntimeScalar)
- Scope exit in loop bodies (existing scopeExitCleanup path)

Add destroyCalled flag on RuntimeBase to prevent double-DESTROY.
Exceptions in DESTROY are caught and warned "(in cleanup)".

Note: Without reference counting, DESTROY may fire early if other
references to the object still exist. A future reference-counting
implementation will make scope-exit DESTROY safe for all contexts.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
fglock added a commit that referenced this pull request Apr 8, 2026
Add comprehensive design document for implementing DESTROY support
for blessed objects and weaken/isweak/unweaken for weak references.

Key design decisions:
- Targeted reference counting on RuntimeBase (only blessed objects
  with DESTROY, not all objects)
- Three-state refCount field: 0=untracked, >0=tracked, MIN_VALUE=destroyed
  (eliminates need for separate destroyCalled boolean)
- Zero overhead for hot path (non-reference assignments)
- GC safety net via Cleaner sentinel pattern for escaped references
- External WeakRefRegistry (no per-scalar memory cost)

Incorporates lessons from PR #450 (eager DESTROY without refcounting
caused 20+ test failures and the DestroyManager attempt (proxy
reconstruction fundamentally broken).

Supersedes dev/design/object_lifecycle.md.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
EOF
)
fglock added a commit that referenced this pull request Apr 8, 2026
Add comprehensive design document for implementing DESTROY support
for blessed objects and weaken/isweak/unweaken for weak references.

Key design decisions:
- Targeted reference counting on RuntimeBase (only blessed objects
  with DESTROY, not all objects)
- Three-state refCount field: 0=untracked, >0=tracked, MIN_VALUE=destroyed
  (eliminates need for separate destroyCalled boolean)
- Zero overhead for hot path (non-reference assignments)
- GC safety net via Cleaner sentinel pattern for escaped references
- External WeakRefRegistry (no per-scalar memory cost)

Incorporates lessons from PR #450 (eager DESTROY without refcounting
caused 20+ test failures and the DestroyManager attempt (proxy
reconstruction fundamentally broken).

Supersedes dev/design/object_lifecycle.md.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
EOF
)
fglock added a commit that referenced this pull request Apr 8, 2026
* design: DESTROY and weaken() implementation plan

Add comprehensive design document for implementing DESTROY support
for blessed objects and weaken/isweak/unweaken for weak references.

Key design decisions:
- Targeted reference counting on RuntimeBase (only blessed objects
  with DESTROY, not all objects)
- Three-state refCount field: 0=untracked, >0=tracked, MIN_VALUE=destroyed
  (eliminates need for separate destroyCalled boolean)
- Zero overhead for hot path (non-reference assignments)
- GC safety net via Cleaner sentinel pattern for escaped references
- External WeakRefRegistry (no per-scalar memory cost)

Incorporates lessons from PR #450 (eager DESTROY without refcounting
caused 20+ test failures and the DestroyManager attempt (proxy
reconstruction fundamentally broken).

Supersedes dev/design/object_lifecycle.md.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
EOF
)

* design: update destroy_weaken_plan.md to v3.0

Updated the design document with findings from bytecode trace analysis:

- Revised refCount encoding: four-state (-1=untracked, 0=tracked/zero
  containers, >0=N containers, MIN_VALUE=destroyed) instead of three-state
- Changed bless-time initialization from refCount=1 to refCount=0, since
  the bless-time RuntimeScalar is a temporary that travels through the
  return chain without being explicitly cleaned up
- Fixed DESTROY ordering: save old referent BEFORE assignment, decrement
  AFTER assignment (Perl 5 semantics: DESTROY sees new variable state)
- Added Section 4A documenting bytecode trace findings (return chain,
  scope exit, overcounting analysis)
- Updated all sections (6-18) for consistency with v3.0 encoding
- Clarified Phase numbering (Cleaner is Phase 4, not Phase 5)
- Cleaned up default field initialization documentation

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: add DESTROY/weaken sandbox tests (196 tests, 8 files)

Comprehensive test suite covering all special cases from the design doc.
All tests validated against system Perl 5.42.

DESTROY tests:
- destroy_basic.t (18): scope exit, undef, overwrite, hash delete,
  multiple refs, blessed array/scalar refs
- destroy_edge_cases.t (22): resurrection, re-bless, exception-in-DESTROY,
  nested DESTROY, local(), overwrite ordering
- destroy_return.t (24): single/two/three-boundary returns, implicit
  return, list context, void context, method chaining
- destroy_collections.t (22): array clear/pop/shift/splice, hash clear,
  nested structures, shared refs, closures
- destroy_inheritance.t (10): parent/child/grandparent DESTROY, SUPER,
  AUTOLOAD fallback, C3 MRO, dynamic @isa

weaken tests:
- weaken_basic.t (34): isweak, weaken, unweaken, copy-is-strong,
  different ref types, double weaken, multiple weak refs
- weaken_destroy.t (24): DESTROY + weak ref interaction, circular refs,
  self-ref, tree back-pointers, weak-only ref
- weaken_edge_cases.t (42): error on non-ref, overwrite, re-bless,
  nested structures, resurrection, unweaken restore

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* design: update destroy_weaken_plan.md to v4.0

Review fixes: Cleaner sentinel reachability, WeakRefRegistry pinning,
missing refcount hooks, VarHandle CAS for thread safety.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* design: update destroy_weaken_plan.md to v5.1

Replaced Cleaner sentinel pattern with stash-walking at shutdown to
avoid pinning objects in memory. Global destruction matches Perl 5
semantics for circular references and missed decrements.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* design: update destroy_weaken_plan.md to v5.2 with review corrections

Key changes based on runtime code review:
- Fix dynamicRestoreState: don't increment restored value (prevents
  permanent +1 overcounting that would block DESTROY for local'd globals
- Add MortalList defer-decrement mechanism (§6.2A) for delete/pop/shift/splice
  return values — equivalent to Perl 5's FREETMPS mortal system
- Add interpreter scope-exit cleanup (§6.5) with SCOPE_EXIT_CLEANUP opcode
  (interpreter backend had no equivalent of emitScopeExitNullStores)
- Fix pop/shift docs: they return raw elements, not copies
- Fix splice location: Operator.java, not RuntimeArray.java
- Defer WeakReferenceWrapper to Phase 5 (all bundled module uses are blessed)
- Update Phase 2 into 2a/2b/2c, expand test plan and file list

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
EOF
)

* design: v5.3 — simplify MortalList scope based on blocked-module survey

Key changes:
- Scope initial MortalList to RuntimeHash.delete() only. Survey of all
  blocked modules (POE, DBIx::Class, Moo, Template Toolkit, Log4perl,
  Data::Printer, Test::Deep) found no real-world pattern needing
  deterministic DESTROY from pop/shift/splice of blessed objects.
- Add MortalList.active boolean gate — false until first bless() into
  a class with DESTROY. Zero cost for programs without DESTROY.
- Defer RuntimeArray.pop/shift and Operator.splice hooks to Phase 5.
- Update Phase 2b, Phase 5, test plan, risks, and file list.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@fglock fglock closed this Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant