diff --git a/build.gradle b/build.gradle index 465e8c328b..0d0c7f74b4 100644 --- a/build.gradle +++ b/build.gradle @@ -384,9 +384,16 @@ test { // Provider we already record as an input so configuration-cache invalidation // stays correct when the env var changes between invocations. def jperlTestFilterProp = providers.environmentVariable('JPERL_TEST_FILTER').orElse('') +def jperlEnableCorrectiveSweeps = providers.environmentVariable('JPERL_ENABLE_CORRECTIVE_SWEEPS').orElse('') +def jperlDisableCorrectiveSweeps = providers.environmentVariable('JPERL_DISABLE_CORRECTIVE_SWEEPS').orElse('') tasks.withType(Test).configureEach { t -> t.inputs.property('jperlTestFilter', jperlTestFilterProp) t.environment('JPERL_TEST_FILTER', jperlTestFilterProp.get()) + // Policy changes must not inherit a previous run's cached Test environment. + t.inputs.property('jperlEnableCorrectiveSweeps', jperlEnableCorrectiveSweeps) + t.inputs.property('jperlDisableCorrectiveSweeps', jperlDisableCorrectiveSweeps) + t.environment('JPERL_ENABLE_CORRECTIVE_SWEEPS', jperlEnableCorrectiveSweeps.get()) + t.environment('JPERL_DISABLE_CORRECTIVE_SWEEPS', jperlDisableCorrectiveSweeps.get()) } // Fast unit tests only (tests in unit/ directory) diff --git a/dev/design/perl-ownership-rewrite.md b/dev/design/perl-ownership-rewrite.md index 6759cfe64f..240b242de9 100644 --- a/dev/design/perl-ownership-rewrite.md +++ b/dev/design/perl-ownership-rewrite.md @@ -1,6 +1,16 @@ # Perl-compatible ownership rewrite -**Status:** Phase 1 closure-pad path is implemented and validated on both backends. Full UAT comparison, local gates, selected CPAN workloads, and PR CI pass. PR #1653 remains a draft pending review. +**Status:** Phase 1 is complete; Phase 2 is in progress. HTML-Tree 5.07 `t/refloop.t` passes 8/8 on both backends with corrective sweeps disabled. `@DB::args` now stores the selected source scalar cells directly, avoiding detached untracked-reference owners. Shared proxy release, ithread weak-slot resurrection, PadWalker rebinding, and async cleanup regressions pass on both backends. Remaining owner families still prevent removal of the legacy bridge and reachability fallbacks. + +**Sweep policy (2026-10-27):** Corrective sweeps are disabled by default, as +requested. `JPERL_ENABLE_CORRECTIVE_SWEEPS=1` temporarily enables legacy +comparison; a nonempty `JPERL_DISABLE_CORRECTIVE_SWEEPS` overrides it. Disabled +runs skip automatic scheduling, discarded-container sweep preparation, +scope-exit capture candidate processing, and the pre-END reachability sweep. +Post-END global teardown remains separate from ordinary reference release. +The build tracks both switches as test inputs so Gradle's configuration cache +cannot silently reuse another run's policy. Default-off is a migration step, +not evidence that every owner family is complete. **Tracking:** [#1649](https://github.com/fglock/PerlOnJava/issues/1649) @@ -102,7 +112,10 @@ and first path establish implementation cost. ## Progress tracking -### Current status: Phase 1 complete; merged in PR #1653 as `84f32be179bdc2b351c3eaae1cb26df39964ab6b` (2026-10-07) +### Current status: Phase 1 complete; Phase 2 in progress + +Phase 1 merged in PR #1653 as `84f32be179bdc2b351c3eaae1cb26df39964ab6b` +(2026-10-07). ### Completed @@ -296,6 +309,33 @@ and first path establish implementation cost. assertion failures remain separately unclassified under [#1646](https://github.com/fglock/PerlOnJava/issues/1646). +### Targeted weak release with owner slots + +`sweepReleasedWeakReferents()` now treats an active owner slot as direct proof +that a pending target is still strongly owned. When every target in the batch +has that proof, it returns without building root snapshots or running the full +walker. Remaining uncounted targets still use the existing fallback. The new +`TargetedWeakReleaseOwnerSlotFastPathTest` passes with sweeps enabled, along +with the existing targeted-query and weak-CODE fallback tests. HTML-Tree +5.07 `t/refloop.t` still passes 8/8 on both backends with sweeps disabled. + +### CODE optree constants receive owner identities (2026-10-31) + +System Perl confirms that a saved coderef keeps a backslash-addressable +constant alive after its named glob is replaced, and that a nested anonymous +optree shares its constant identity across closure instances while its factory +CV remains alive. The expanded permanent Perl regression passes 8/8 on system +Perl. Each compiled optree now has a distinct constant scalar per source +occurrence and one CODE_PAD_CONSTANT slot per strong edge. A parent CV owns +embedded anonymous optrees as templates; each produced closure also owns its +own edges. The interpreter tracks constants from its constant pool and nested +closures in the same owner ledger. Glob replacement retires only the stash +owner, and final CV cleanup releases constant slots and clears weak observers +after the last owner. Runtime lookup uses the owning CV's registered optree +constants; the reachability walker no longer follows pad constants as a +substitute for explicit edges. Full build and backend validation for this +batch is pending. + ### Next steps 1. Extend the ownership path through scalar proxies and ordinary array/hash @@ -427,3 +467,1296 @@ and first path establish implementation cost. investigation is safe snapshot reuse across unchanged root sets or an ownership path that removes the ordinary cleanup fallback. Full release acceptance also remains required before closing the issue. + +### Phase 2 progress (2026-10-27) + +- JVM lexical cleanup now includes captured scalar cells at ordinary scope and + subroutine fall-through boundaries. The new `closure_capture_scope_exit.t` + regression passes on system Perl and both backends. The existing recursive + builder regression also passes on both backends with corrective sweeps + disabled. +- Explicit return cleanup now leaves the interpreter's `RETURN` opcode + decrements queued until the `RuntimeCode` boundary has removed the callee's + lexical registrations. The explicit-return regression passes 6/6 on JVM and + interpreter with corrective sweeps disabled. Declaration-scope provenance + still distinguishes locals from outer cells borrowed by a closure. +- JVM and interpreter closure creation now assign a distinct `SCALAR_CAPTURE` + identity to each captured scalar pad cell. `releaseCaptures`, pad rebinding, + and thread cloning balance those identities; a focused Java test passes. + The slots remain owned by their closure metadata while the selective count + bridge is active. Registering them as a second global root retained a + Catalyst request tempfile, so native root-ledger integration still needs a + closure-liveness boundary. +- [x] Counted scalar, array-element, and hash-element stores now have distinct + `PerlOwnerSlot` identities (2026-10-27). Overwrite preserves the cell's + identity; deferred releases carry an immutable slot identity and sequence, + so draining an old value cannot retire a newer assignment. These slots + identify the selective-count bridge without becoming global roots. + `RuntimeHash.put` acquires its element owner and releases displaced values; + array removal and scope cleanup retain release provenance. A final + identified scalar/element release can destroy its referent directly rather + than restoring a synthetic count from a stale root snapshot. +- [x] Local-array rvalue returns now transfer through distinct `RETURN_VALUE` + slots before lexical cleanup on both backends (2026-10-27). Void returns + retain no element owners; scalar returns yield the count; list returns keep + mortal ownership until the receiving caller consumes the values. The new + [`local_array_return_owner_release.t`](../../src/test/resources/unit/refcount/local_array_return_owner_release.t) + passes 8/8 on system Perl and both backends with corrective sweeps disabled; + the unfixed implementation failed five assertions on each backend. The + equivalent implicit-return probe also passes 8/8 on all three runtimes. +- [x] Weak links and captured/container-owned scalar results are treated as + borrows during recursive release and void discard (2026-10-27). This keeps + self-retaining closure cycles alive after invocation. The existing closure + cycle regression passes 3/3, the weak localized-cache regression 7/7, and + the new repeated-element/cycle regression 6/6 on both backends with sweeps + disabled. Seven Java owner-slot tests cover replacement, shared referents, + queue provenance, weak edges, borrowed captures, and a final element release + with zero root queries or snapshot builds. +- [x] Unchanged HTML-Tree 5.07 `t/refloop.t` passes 8/8 on JVM and interpreter + with `JPERL_DISABLE_CORRECTIVE_SWEEPS=1` (2026-10-27). The full unfiltered + `nice -n 19 make` passes in 6m45s. Focused no-sweep qualification also passes + `destroy_edge_cases.t` (27/27), `weaken_edge_cases.t` (43/43), and END + destruction ordering (5/5) on both backends. +- [x] Successful and throwing `DESTROY` callbacks now share ownership + finalization (2026-10-27). Synthetic callback arguments are balanced before + reblessing and resurrection checks; dead referents clear weak observers and + release owned children even when the callback dies or warning delivery + throws. Real escaped owners preserve the referent and allow its next + destruction. `issue_1649_destroy_exception_cleanup.t` now passes 10/10 + without sweeps on both backends, resolving its two weak-clearing failures. + The new + [`destroy_exception_resurrection.t`](../../src/test/resources/unit/refcount/destroy_exception_resurrection.t) + passes 11/11 on system Perl and both backends without sweeps, covering + resurrection followed by final release, reblessing, cleanup warnings, and + preservation of `$@`. A Java regression verifies observer clearing and + child-owner release with automatic sweeps suppressed. HTML::Tree remains + 8/8 on both backends without sweeps; the latest unfiltered + `nice -n 19 make` passes in 7m05s. + +- [x] Shared scalar/list return boundaries now snapshot reference values, + including CODE values, into mortal `RETURN_VALUE` slots (2026-10-27). + Forwarding an existing temporary avoids duplicate acquisition; lvalue returns + retain their cell aliases. Staged reference-valued list arguments acquire + distinct `ARGUMENT_VALUE` slots and queue their release once, including + repeated referents. Pending return owners protect closure captures until + the caller consumes the result. Five Java transfer tests pass. The new + [`code_return_argument_snapshot.t`](../../src/test/resources/unit/refcount/code_return_argument_snapshot.t) + passes 4/4 on system Perl and both backends without sweeps; the unfixed + implementation failed two assertions on each backend. Focused no-sweep + checks also pass local-array returns (8/8), throwing destructor resurrection + (11/11), self-retaining closure cycles (3/3), explicit closure returns + (6/6), and unchanged HTML::Tree (8/8) on both backends. The bundled + candidate passes the unfiltered `nice -n 19 make` gate in 8m18s. + +- [x] Partially evaluated method arguments now retire their explicit owners + on exceptions and frame exit (2026-10-27). JVM argument emission installs + an exception handler; interpreter inline-eval unwinding retires staging + registers created within that eval. Strong staging registrations and call + depth distinguish abandoned calls from still-active outer evaluations. + Staged argument owners and pending return tokens retain their referents + through the runtime positive-owner registry, releasing that retention once + their exact token drains. Existing corrective sweeps respect these explicit + transfer owners rather than reclaiming them from an incomplete root walk. + Non-local map returns preserve their result mortals during interpreter and + shared-call cleanup. The new + [`argument_evaluation_unwind.t`](../../src/test/resources/unit/refcount/argument_evaluation_unwind.t) + passes 20/20 and + [`argument_evaluation_return.t`](../../src/test/resources/unit/refcount/argument_evaluation_return.t) + passes 18/18 on standard Perl and both backends without sweeps. Unfixed + backends failed six unwind assertions each and six/seven return assertions + on JVM/interpreter respectively. Four Java checks cover repeated argument + identities, caller-frame preservation, non-local return transfer, and child + retention through an explicit argument owner. All Java tests and the + focused normal-mode refcount corpus pass (`nice -n 19 make`, 2m16s). + +### Final owner releases and default-off qualification (2026-10-27) + +- [x] Final weakening and scalar/element overwrite use their identified owner + edge instead of restoring a count from a root walk. Unblessed scalar cells + finalize when their lexical binding or last capture ends; escaped strong + references keep the cell and its contents alive. Scalar capture identities + are visible in the referent's local ledger without becoming global roots. + Legacy sweeps respect those binding/capture owners. Java coverage verifies + final release with zero root queries and no duplicate lifecycle transition. +- [x] Whole-array replacement, undef, and local restoration release only owned + element cells. Clearing aliased `@_` preserves the caller's scalar owners, + including independently weakened probe copies; inserted elements still + release once. Files: `RuntimeArray.java`, `PerlArgumentUnwindOwnerTest.java`. +- [x] JVM and interpreter acquire rvalue return owners before lexical cleanup. + Both interpreter return emitters reserve a separate result register, so + cleanup of an earlier local cannot replace a later local's owner cell. + Referenced scalar cells keep their contents until the receiving caller + releases or overwrites them. Lvalue aliases and socket ownership remain + covered by the existing regressions. Files: `RuntimeCode.java`, + `EmitStatement.java`, `EmitControlFlow.java`, `BytecodeCompiler.java`, + `CompileOperator.java`, `BytecodeInterpreter.java`, `MortalList.java`. +- [x] Temporary-expression protection no longer follows scalar references into + arbitrary hash/array graphs. The referent and its element slots supply + ownership; only direct expression references are inspected. A Java + regression rejects traversal into unrelated metadata. This removes a + recursive Moose metadata walk observed during Catalyst bootstrap: the JVM + probe now reaches its tempfile assertion instead of timing out at 240s. + +New permanent Perl regressions pass on standard Perl and both backends with +sweeps disabled by default: + +| Regression | Assertions | +| --- | --- | +| `refcount/scalar_cell_scope_owner_release.t` | 11/11 | +| `refcount/scalar_cell_return_contents_owner.t` | 8/8 | +| `refcount/argument_array_clear_borrowed_cells.t` | 9/9 | +| `refcount/lexical_return_slot_owner_release.t` | 6/6 | + +The corresponding unfixed failures were retained before each correction. +Existing default-off checks pass on both backends: probe copies (5/5), scalar +assignment guards (6/6), weaken/DESTROY (24/24), scalar weak references +(10/10), sliding-window release (12/12), argument exception/return cleanup +(20/20 and 18/18), local-array returns (8/8), explicit closure returns (6/6), +self-retaining closure cycles (3/3), and unchanged HTML::Tree (8/8). Existing +socket lifetime and blessed-descriptor checks also pass on both backends. + +### Active call owners and final default-off gate (2026-10-27) + +- [x] Every active call frame owns an `ACTIVE_CODE` identity. Recursive calls + retain distinct tokens; removing one frame retires one token. A counted final + release waits for the last active frame, then clears observers and captures + without a root query. Borrowed CODE with no counted release keeps its captures; + template, stash, and pinned CODE ownership still require migration. +- [x] Capture sweeps cannot override explicit live call/capture tokens, including + in legacy comparison mode. Default-off also skips capture candidate queuing + and processing and the pre-END reachability cleanup. The default-policy Java + regression verifies no candidate retention or pre-END root queries. +- [x] Preserve genuine self-capture cycles rather than breaking them at lexical + exit. Assigning CODE into an already captured cell acquires the capture's + referent edge separately from the lexical store. Explicitly breaking the + cycle allows the final external owner to release its captured data. + +New permanent regressions pass on standard Perl and both backends without +sweeps: `active_callback_owner_release.t` (13/13), +`self_capture_cycle_owner.t` (9/9), and `code_template_owner_lifetime.t` (4/4). +The callback and self-cycle failures were retained on the unfixed candidate. +`ActiveCodeOwnerTest` passes all four cases, including zero root queries on +final release. Existing `weak_active_coderef_dispatch.t` now passes 3/3 on both +backends, and unchanged HTML::Tree still passes 8/8 on both. + +### Eval, tied handlers, and native parser arguments (2026-10-27) + +- [x] Tied hash, array, scalar, and handle wrappers acquire distinct + `TIE_HANDLER` identities on independent handler snapshots. Repeated release + retires the owner once. A released snapshot remains borrowable while dynamic + localization restores magic; clearing its payload caused five observed + regressions, all now passing on both backends without sweeps. +- [x] Final container destruction releases magic and child slots independently + of temporary expression borrows. Ordinary lexical cleanup still protects + live aggregates. The final destruction path runs after resurrection checks. + Java coverage verifies hash/array child retirement and final tie release + without root queries. +- [x] JVM eval callback returns clean eval lexicals before returning to their + caller. Interpreter evals consume untargeted callback returns as successful + eval results rather than errors. Explicit return owners protect referenced + results throughout lexical cleanup, abandoned method arguments, invocant + release, and local restoration. Targeted try/finally paths remain a separate + ownership boundary to complete. +- [x] Scalar overwrite's external-root fallback cannot clear weak observers + while identified strong slots remain. This exposed an actual native-call + argument leak in `HTMLParser.fireEvent`: freshly owned event-data and call + arrays were never retired. Both arrays now release on success, exceptions, + and absent-method dispatch. Native parser containers use anonymous references + instead of inventing lexical bindings. Accumulated event arrays retain their + own contents until the program clears them. + +Permanent focused Perl coverage passes on standard Perl and both backends +with corrective sweeps off: + +| Regression | Assertions | +| --- | --- | +| `refcount/tied_handler_owner_release.t` | 13/13 | +| `refcount/eval_callback_return_owner_release.t` | 12/12 | +| `refcount/native_parser_callback_owner_release.t` | 9/9 | + +Unfixed failures were retained before the corrections. `TiedHandlerOwnerTest` +adds three Java cases for distinct identities, exactly-once retirement, magic +restoration borrows, and final child release. The latest focused comparison +gate passes in 1m36s: +`JPERL_ENABLE_CORRECTIVE_SWEEPS=1 JPERL_TEST_FILTER=refcount/ nice -n 19 make`. +Log: `/tmp/make-issue1649-native-parser-owner-batch.log`. + +Final direct default-off checks restore unchanged HTML::Tree to **8/8 on both +backends**. All eight existing `html_parser_*.t` regressions pass on both +backends. The preceding default-off reader batch also passes eval/map cleanup +(3/3), simple tied-hash destruction (2/2), explicit closure returns (6/6), +destructor exception/resurrection (11/11), and self-capture cycles (9/9). +The JVM tied-hash autovivification loop passes 6/6. Five compatibility +regressions exposed by clearing retired handler snapshots now pass on both +backends: `eval_tied_local_cleanup.t`, +`issue_1630_repeated_local_alias_io.t`, `signature_eval_tie_caller.t`, +`symbol_table_glob_identity.t`, and `tied_lvalue_sub_return.t`. + +### Conditional scopes and method holds completed (2026-10-27) + +Method invocant holds now have independent `ARGUMENT_VALUE` identities and +retire exactly once, including abandoned calls. Three Java cases cover distinct +holds, deferred final release, and immediate abandoned-call release without +reachability queries. + +The interpreter now closes condition-local scopes. Opcode 639 snapshots their +result before cleanup; the JVM likewise acquires conditional results before +retiring locals. Tied scalar, hash-element, and array-element lvalue results +retain their storage through the caller's STORE. Legacy capture correction +skips pads with identified capture owners. + +Standard Perl and both default-off backends pass the new conditional regressions +(12/12, 6/6, and 7/7). The original tied-hash autovivification regression now +passes 6/6 on both backends. Unchanged HTML::Tree remains 8/8 on both. + +The subsequent unfiltered default-off gate completed in 9m49s with eight +failures: three Perl acceptance cases listed below and five Java cases that +expect legacy sweeps. Log: +`/tmp/make-issue1649-method-conditional-final-full.log`. + +### Final CV state-storage cleanup completed (2026-10-27) + +Final CV destruction now detaches and retires persistent scalar, array, and hash +bindings after CODE destructor callbacks and the resurrection decision. A blessed +CV that resurrects keeps its original state until its final destruction. Escaped scalar cells and aggregates retain their values until their +own last owner ends. Persistent scalar cells are born with a binding rather +than being mistaken for ephemeral returned scalars. + +State aggregate refaliases own separate counted reference slots. Releasing a +CV retires those slots without ending the source aggregate's lexical binding. +Lazy definition adoption shares this ownership metadata with the state maps; +ithread cloning copies it with the cloned graph. + +Permanent regressions `refcount/closure_state_owner_release.t` (10/10), +`refcount/state_aggregate_alias_owner_release.t` (6/6), and +`refcount/blessed_code_state_resurrection.t` (10/10) pass standard Perl and +both backends with sweeps explicitly disabled. Unfixed-parent failures are +retained in `/tmp`. The final focused comparison gate passes in 2m35s: +`/tmp/make-issue1649-state-resurrection-final-batch.log`. Destructor exceptions, +true cycles, and unchanged HTML::Tree 8/8 also pass on both backends: +`/tmp/issue1649-state-resurrection-final-backend-results.txt`. + +The full default-off gate before the final CODE-destructor ordering correction +finished in 7m07s with the same eight known failures and no new failures from +state-storage cleanup. The final ordering correction received the focused and +backend checks above; another full gate remains required before publication. +Evidence: `/tmp/make-issue1649-state-storage-final-full.log` and +`/tmp/issue1649-state-storage-full-failures.txt`. + +Adjacent default-off checks preserve state refaliases, declared-reference list +identity, true self-capture cycles, CODE template lifetimes, and HTML::Tree 8/8 +on both backends. `state.t` passes the JVM; its interpreter failures at 6, 7, +and 13 reproduce on the saved pre-change JAR. Evidence: +`/tmp/issue1649-state-final-backend-results.txt` and +`/tmp/issue1649-state-parent-interpreter.log`. + +This completes state-storage retirement for CVs that actually reach final +release. Named CODE/glob lifetime accounting still prevents that transition in +the remaining named-state acceptance case. No remaining sweep or compatibility +bridge has been removed on the strength of these focused passes. + +### Named CODE-slot ownership completed (2026-10-27) + +One physical CODE slot now owns one counted `CODE_SLOT` identity. Whole-glob +names sharing its scalar share that edge; separate CODE slots have separate +identities. Saved localization, compiled-GV pins, and retained compiled-call +references keep the slot alive across namespace replacement or deletion. +Restoration releases the temporary slot after the saved slot is installed. + +Native CODE final release uses these counted identities instead of historical +stash counters or the discarded-closure root-walk correction. Capture attachment +preserves counts already acquired by named slots. Cache notifications no longer +acquire an extra owner during first assignment to an empty slot. Compatibility +root-index shape metadata survives capture retirement without retaining captured +values or influencing lifetime decisions. + +Interpreter frames borrow a stable self-reference for `__SUB__` and persistent +state operations. Undefining the executing glob therefore leaves its active CV +and state usable until the last frame returns. Thread snapshots reconstruct +tracked CV counts, scalar owner identities, and namespace/compiled-GV bindings +in the child runtime. + +Six `CodeSlotOwnerTest` cases cover independent and shared slot identities, +first assignment and replacement, localization, compiled pins, active calls, +and final release without reachability queries. Three new permanent Perl tests +pass standard Perl and both backends with corrective sweeps explicitly disabled: + +| Regression | Assertions | +| --- | --- | +| `refcount/named_code_state_owner_release.t` | 9/9 | +| `refcount/localized_code_state_owner_release.t` | 9/9 | +| `refcount/compiled_glob_state_owner_release.t` | 4/4 | + +The original `weak_deferred_capture_full_sweep.t` now passes 3/3 on both +backends. Existing glob redefinition, local alias restoration, current-sub map +and sort, glob identity, inheritance cloning, CODE resurrection, and unchanged +HTML::Tree 8/8 also pass on both backends. Evidence: +`/tmp/issue1649-code-pin-final-backend-results.txt`. +The focused comparison build passes in 1m33s: +`/tmp/make-issue1649-code-slot-pin-batch.log`. + +The final unfiltered default-off gate completed in 7m19s with **seven failures**: +the two existing shared-reference cases below and the five Java cases requiring +legacy sweeps. Named state teardown now passes in the full gate, reducing its +failure count from eight to seven. No new full-suite failure was introduced. +Logs: `/tmp/make-issue1649-code-slot-final-full.log` and +`/tmp/issue1649-code-slot-full-failures.txt`. + +Unfixed-parent evidence is retained for named-state release, the first-store +localization leak, and premature state destruction after stash deletion. +The earlier ownership work is preserved in WIP checkpoint `98e611e96`. + +This completes the named-slot retain/release paths tested here. Compiler +templates, per-optree retirement of compiled-GV pins, raw descriptor/embedding +borrows, and complete CODE producer ownership still need audit before the +compatibility bridge can be removed. The complete rewrite remains in progress. + +### Shared proxy and cloned weak-slot cleanup (2026-10-27) + +`SharedElementProxy` is now a local expression borrow; the destination Perl +slot owns the view. Final release clears weak observers on that local view +without cascading into canonical shared storage. + +Ithread snapshots install all cloned weak edges before draining the queued +capture-owner releases. This lets `DESTROY` rescue an existing weak hash slot +after the complete child graph is registered. Scope cleanup also retires +legacy scalar capture counts that have no explicit `SCALAR_CAPTURE` owner. +Async warning-handler retention uses its counted scalar owner, and PadWalker +rebinding records replacement scalar and aggregate captures with explicit owner +slots. + +The following regressions pass on both PerlOnJava backends with corrective +sweeps disabled: `threads_shared_nested_proxies.t` (10/10), +`threads_destroy_weak_slot_rescue.t` (3/3), `padwalker_set_closed_over.t` +(6/6), `direct_closure_padwalker_rebind.t` (3/3), +`custom_warning_async_suspend_state.t` (6/6), and +`refcount/io_async_loop_refcount_lifecycle.t` (2/2). The two deferred-capture +boundary Java cases also pass without sweeps. The two thread regressions also +pass their standard Perl oracle runs. + +Current-JAR requalification also passes `closure_capture_explicit_return.t` +(6/6) and unchanged HTML-Tree 5.07 `t/refloop.t` (8/8) on both backends with +sweeps disabled. Logs: `/tmp/issue1649-current-explicit-return-jvm.log`, +`/tmp/issue1649-current-explicit-return-interpreter.log`, +`/tmp/issue1649-current-html-tree-jvm.log`, and +`/tmp/issue1649-current-html-tree-interpreter.log`. + +Requalification after the scalar-reference return and lazy thread-snapshot fixes +passes the full sweep-enabled `make` gate. The full no-sweep gate completed +1,242 tests with only these three existing sweep-helper cases failing: +`ReachabilityQueryCostTest`'s two targeted weak-release checks and +`WeakBackrefCodeCaptureSweepTest`'s quiet weak-code capture walk. No Perl unit +test failed. The fallback scalar-reference guard, lazy thread-snapshot race, +and unchanged HTML-Tree 5.07 `t/refloop.t` each pass on both backends with +sweeps disabled. Evidence: `/tmp/make-issue1649-final-normal.log`, +`/tmp/make-issue1649-final-no-sweep.log`, and the corresponding +`/tmp/issue1649-*-no-sweep-verified.log` files. + +### Interpreter temporary closure owners retired + +Counted scalar owner slots now hold a weak link to their implementing scalar +cell. At interpreter frame exit, generated closures that are neither active nor +returned release unregistered CODE cells left in borrowed aliased `@_` arrays. +Registered lexicals, package slots, and container-owned CODE refs stay live. +The Catalyst request tempfile regression passes 2/2 on the interpreter with +corrective sweeps disabled; the filtered JVM unit test also passes. Logs: +`/tmp/issue1649-catalyst-interpreter-borrowed-code-cell.log` and +`/tmp/make-issue1649-catalyst-borrowed-code-cell.log`. + +### Temporary CODE argument slots counted + +Temporary closures placed in aliased argument arrays now acquire a counted +`ARGUMENT_VALUE` owner. Returning from the call releases that owner through the +normal scalar cleanup path. `cleanupOwnedArgumentAliases()` no longer uses a +root walk to decide whether to release the CV's captures. `CallArguments` +retains its materialized argument array until finish, so a call rejected before +callee entry also retires the owner; normal callee cleanup and this fallback +are idempotent on the same slot. + +`PerlClosureArgumentOwnerTest` verifies normal and abandoned-call release, the +slot identity, capture retirement, and zero reachability queries. The Perl regression +`issue_1649_temporary_closure_argument_owner.t` passes 5/5 on system Perl and +both PerlOnJava backends with sweeps disabled. The focused Java gate also passes +`PerlArgumentUnwindOwnerTest`, `PerlCallTransferOwnerTest`, and both deferred +capture boundary cases. Current-JAR requalification passes explicit-return +cleanup 6/6 and HTML-Tree 5.07 `t/refloop.t` 8/8 on both backends with sweeps +disabled. Evidence: `/tmp/issue1649-argument-owner-oracle.log`, +`/tmp/make-issue1649-argument-owner-materialized-final.log`, and logs with the +`/tmp/issue1649-argument-owner-materialized-` prefix. + +This closes the temporary CODE-cell argument path only. Aliased `@_` cells, +native buffers, escaping aliases, and the remaining return paths still need +explicit owner-slot accounting. + +### Direct scalar arguments staged through later expressions (2026-10-28) + +`CallArguments` gives each strong scalar return snapshot its own +`ARGUMENT_VALUE` slot until `finish()`. This keeps an earlier argument alive if +a later expression drains its return owner, and keeps a final scalar argument +alive through callee cleanup. Persistent caller cells remain borrowed so +assignment through `@_` can release their previous values normally. + +`CallArgumentsScalarStagingOwnerTest` covers a non-final mortal result, a final +mortal result, and a persistent caller cell. The default-off full +`nice -n 19 make` gate passes. Explicit-return cleanup passes 6/6 and +HTML::Tree 5.07 `t/refloop.t` passes 8/8 on both backends with corrective +sweeps unset. Evidence: `/tmp/make-issue1649-final-argument-owner-verified.log` +and files with the `/tmp/issue1649-final-argument-` prefix. + +This covers scalar return snapshots in direct call arguments. It does not +complete aliased `@_` cell ownership or shared and tail-call cleanup. + +Unresolved acceptance cases: + +- Escaping proxy/lvalue magic and other native call buffers still need explicit + ownership completion; compatibility borrows are not a substitute for strong + alias owners. + +The two targeted-walker assertions in `ReachabilityQueryCostTest` and the quiet +weak-code capture walk in `WeakBackrefCodeCaptureSweepTest` intentionally call +the diagnostic sweep helpers. Runtime cleanup requests now use a separate +corrective-sweep scheduler, which remains gated by the environment policy; the +default-policy test verifies that disabled runtimes do not queue or run those +requests. Keep the existing helper tests intact. + +Phase 2 remains in progress: the integer count is still the compatibility +bridge for these new identities, and argument ownership and other return paths +are not fully migrated. Passing the default-off suite does not establish that +every producer, alias, or cleanup boundary uses semantic ownership alone. + +### Explicit scalar-capture owner release + +Scope-exited scalar captures now use their `SCALAR_CAPTURE` owner slots to +identify each owning `RuntimeCode`. When every capture of a pad has an explicit +owner, cleanup checks the CODE owner ledger and releases ownerless closures +without building END or scalar root graphs. Pads with compatibility-only +captures still use the existing graph fallback. The Java regression +`CaptureOwnerReleaseWithoutWalkerTest` passes; the full sweep-enabled +`nice -n 19 make` gate passes, and HTML-Tree 5.07 `t/refloop.t` passes 8/8 on +both backends with corrective sweeps disabled. + +This removes root walks for migrated capture-owner decisions, but weak +referent candidate discovery and the weak-CODE capture graph are still +sweep-based. + +### Weak referents with explicit owner slots + +Quiet weak sweeps now check a referent's strong owner-slot ledger before +consulting the legacy captured-element proof. A candidate with an active slot +is preserved directly, so the weak-CODE capture graph is built lazily only for +aggregate candidates that still lack owner identities. The new +`WeakBackrefOwnerSlotFastPathTest` covers an array-element owner, while the +existing weak-CODE capture test continues to cover the legacy fallback. The +sweep-enabled full `nice -n 19 make` gate passes, and HTML-Tree 5.07 +`t/refloop.t` passes 8/8 on both backends with sweeps disabled. + +### Closure capture release with an explicit scalar owner (2026-10-28) + +When undefining one CODE scalar, cleanup now checks whether another active +owner-slot identity retains the CV before building a root snapshot. It excludes +the slot being overwritten, so final-owner release still retires captures +through the existing compatibility path. `CodeOwnerSlotReleaseFastPathTest` +checks that the first of two scalar owners releases with zero root queries and +that the final owner still releases the captures. The full sweep-enabled +`nice -n 19 make` gate passes. Ownerless CODEs without explicit identities +still use the root-walk fallback. + +### Package-root release from counted owner slots (2026-10-28) + +When a package-rooted blessed value reaches a transient zero during overwrite, +remaining counted owner-slot identities now repair the compatibility count +before the package-root walker runs. `PackageRootOwnerSlotFastPathTest` keeps a +weak observer alive through an undercounted overwrite, checks that the count +matches the remaining hash slot, and verifies zero root queries. The full +sweep-enabled `nice -n 19 make` gate passes. Values without counted owner slots +retain the existing root-walk fallback. + +### Localized CODE restore with an active owner slot (2026-10-28) + +Whole-glob restore now skips the scalar registry, live capture, and Perl-root +walks when a non-stash owner slot still retains the localized CV. The regression +`LocalizedGlobCodeOwnerSlotFastPathTest` models the legacy direct stash write, +keeps the CV active across restore, and verifies its captures survive with zero +root queries. The sweep-enabled full `nice -n 19 make` gate passes. CVs without +owner slots still take the compatibility checks. + +### Default-off full-suite acceptance (2026-10-28) + +Production cleanup callers now schedule corrective passes through a gated +runtime method. The direct helper APIs remain available to the existing +reachability-cost and weak-CODE capture tests, including the test that requests +one explicit quiet pass at a safe boundary. This keeps diagnostic coverage +while leaving corrective scheduling disabled by default. + +The full `nice -n 19 make` gate passes with both sweep-control variables unset: +212 test suites report 3,095 tests, zero failures, zero errors, and five +skips. `CorrectiveSweepDefaultPolicyTest`, `ReachabilityQueryCostTest`, and +`WeakBackrefCodeCaptureSweepTest` all pass. Against the same build, HTML::Tree +5.07 `t/refloop.t` passes 8/8 and `closure_capture_explicit_return.t` passes +6/6 on both backends with corrective sweeps explicitly disabled. Logs: +`/tmp/make-issue1649-no-sweep-helper-separation-retry.log` and files with the +`/tmp/issue1649-post-gate-` prefix. + +This qualifies the current default-off behavior; it does not remove direct +diagnostic walkers, the integer-count compatibility bridge, or the remaining +reachability fallbacks. + +### Tail-call release from counted owner slots (2026-10-28) + +`releaseTailCallArgElement` now restores a transiently undercounted referent +from its remaining counted owner slots before consulting the weak-referent +root walker. `TailCallArgumentOwnerSlotFastPathTest` models a tail-call +handoff with one surviving array-element owner and verifies that the weak +observer stays live with zero root walks. The default-off full +`nice -n 19 make` gate passes; `goto_tailcall_args_cleanup.t` passes 5/5 and +`goto_weakself_tailcall_refcount.t` passes 2/2 on both backends with sweeps +unset. System Perl passes the weak-self test; its tail-call cleanup test skips +because `Internals` is unavailable. Evidence: `/tmp/make-issue1649-tailcall-owner-fastpath.log`, +`/tmp/issue1649-tailcall-args-*.log`, and +`/tmp/issue1649-tailcall-weakself-*.log`. + +This avoids reachability walks when counted slots prove a surviving owner. +Tail-call arguments without counted slots still use the compatibility walker. + +### Deferred weak release from counted owner slots (2026-10-28) + +Ordinary deferred cleanup now restores the compatibility count from remaining +counted owner slots before asking the root walker for blessed referents. +`DeferredReleaseOwnerSlotFastPathTest` checks that one surviving scalar-cell +slot preserves its weak observer with zero root walks, and that releasing the +final slot still clears it. The unblessed aggregate snapshot path remains in +place to distinguish live Perl roots from stale scalar wrappers. The default-off +full `nice -n 19 make` gate passes. Evidence: +`/tmp/make-issue1649-deferred-owner-fastpath-corrected.log`. + +### Argument-owner ledger bypass for active-frame scans (2026-10-28) + +Deferred weak releases now consult counted owner slots before scanning active +`@_` arrays and staged argument frames for a matching referent. If a blessed +referent has another counted owner, ordinary deferred cleanup repairs the +compatibility count directly. `ActiveArgumentOwnerLedgerFastPathTest` verifies +that a persistent caller cell remains a borrowed alias, avoids the frame scan, +and still clears its weak observer when the final slot is overwritten. The +default-off full `nice -n 19 make` gate passes. Evidence: +`/tmp/make-issue1649-argument-scan-fastpath.log`. + +Uncounted compatibility aliases still use the referent scan; explicit alias +slot ownership remains unfinished. + +### Scalar overwrite recovery from counted owner slots (2026-10-28) + +When a blessed weakly observed referent reaches a transient zero during scalar +overwrite, `RuntimeScalar` now restores the compatibility count from surviving +counted owner slots before invoking nested-call or package-root reachability +fallbacks. `RuntimeScalarCountedOwnerRepairTest` verifies that a remaining +array-element slot preserves the weak observer with zero root queries, and +that releasing the final slot clears it. The default-off full +`nice -n 19 make` gate passes. Evidence: +`/tmp/make-issue1649-scalar-owner-repair-final2.log`. + +### Non-counted identities for staged and active argument cells (2026-10-28) + +`CallArguments` and active `@_` frames now attach `ARGUMENT_ALIAS` identities +only to durable Perl storage cells and already active argument aliases. These +identities follow reference assignments and weak/strong transitions without +changing Perl-visible `refCount`; overwrite and explicit `undef` defer a final +release while an alias is the only owner. Transient expression wrappers retain +their staged return ownership without being treated as caller cells. The alias +identity is recorded in the referent ledger but does not add a separate runtime +root; the active frame already owns its scalar cell. RuntimeBase keeps +constant-time counts for alias and counted slots so ordinary deferred cleanup +does not scan a referent's full owner ledger. + +`ArgumentAliasOwnerSlotTest` covers refcount neutrality, identity transfer from +primitive to reference values, weak-target lifetime through overwrite and +explicit `undef`, exclusion of transient wrappers, no extra runtime roots, and +retirement when a stale cell still points at a destroyed referent. The default-off +full `nice -n 19 make` gate passes. Evidence: +`/tmp/make-issue1649-argument-alias-frame-roots-final.log`. + +This covers durable scalar cells staged for calls and active argument frames. +The tracked `new_from_lol` lifecycle reproducer passes 4/4 on JVM; its +interpreter result (1/4) matches the committed parent `7ba27d774`, so those +three failures predate this change. The TreeBuilder lifecycle reproducer passes +4/4 on both backends. + +### Retire active argument identities when `@_` slots are removed (2026-10-28) + +Active argument-frame identities now retire when their matching array slot is +removed, replaced, cleared, or deleted through a subrange operation. The +element-list hooks cover indexed removal and Java's direct `removeFirst` / +`removeLast` paths. `ArgumentAliasOwnerSlotTest` checks duplicate aliases, +replacement, clearing, and exact one-time retirement when the frame later exits. +`nice -n 19 make` passes. With corrective sweeps unset, explicit-return +cleanup passes 6/6 on both backends and HTML-Tree 5.07 `t/refloop.t` passes 8/8 +on both backends. + +### Argument cleanup decisions use owner identities (2026-10-28) + +`hasActiveArgumentOwner` now checks only the `ARGUMENT_ALIAS` owner-slot count. +The fallback walks through active `@_` arrays and staged argument values were +removed, along with their recursive `CallArguments` referent scan. The +default-off full `nice -n 19 make` gate passes. Against that build, +`weak_nested_form_argument_lifetime.t` passes 29/29 and +`closure_capture_explicit_return.t` passes 6/6 on both backends; HTML-Tree +5.07 `t/refloop.t` passes 8/8 on both backends with sweeps unset. + +### Staged owners for nested list aggregates (2026-10-28) + +`CallArguments` now walks nested `RuntimeList`, `RuntimeArray`, and +`RuntimeHash` values and creates one `ARGUMENT_VALUE` owner for each strong +reference element. `CallArgumentsNestedAggregateOwnerSlotTest` verifies that +array and hash elements nested in lists each retain their own slot through +later argument cleanup and release at call completion. The default-off full +`nice -n 19 make` gate passes. Explicit-return cleanup is 6/6, nested-form +lifetime is 29/29, and HTML-Tree 5.07 `t/refloop.t` is 8/8 on both backends +with sweeps unset. + +### Shared and tail-call argument frame identities (2026-10-28) + +`ArgumentAliasHandoffOwnerSlotTest` verifies that a tail-call target frame gets +its own alias identity and releases it in LIFO order, and that overwriting a +shared scalar cell retires its identity exactly once. The default-off full +`nice -n 19 make` gate passes. With sweeps unset, `goto_tailcall_args_cleanup.t` +passes 5/5, `goto_weakself_tailcall_refcount.t` passes 2/2, and +`threads_shared_nested_proxies.t` passes 10/10 on both backends. + +### Tail-call frame identities and thread cloning (2026-10-28) + +Queued scalar decrements now capture their source argument-frame identity at +enqueue time. Tail-call cleanup matches that identity directly, while retaining +the existing direct-argument and bless-temporary cases. The new +`ArgumentAliasHandoffOwnerSlotTest` verifies copied arguments retain the source +identity in the pending queue and that thread cloning drops this execution-local +identity. `nice -n 19 make` passes; `goto_tailcall_args_cleanup.t`, +`goto_weakself_tailcall_refcount.t`, and `threads_shared_nested_proxies.t` pass +on both backends with corrective sweeps unset. + +### Weakening with a surviving explicit owner (2026-10-28) + +`WeakRefRegistry.weaken()` now skips `reachableOwnerCount()` when another +explicit owner slot remains. `PerlFinalWeakOwnerTest` verifies that weakening a +live lexical owner preserves the weak observer with zero scalar reachability +queries. The default-off full `nice -n 19 make` gate passes; with corrective +sweeps unset, explicit-return cleanup passes 6/6, nested-form lifetime passes +29/29, and HTML-Tree 5.07 `t/refloop.t` passes 8/8 on both backends. + +### Identities for untracked container referents (2026-10-28) + +Array and hash elements that strongly reference an otherwise untracked or +weakly-tracked referent now receive an `UNTRACKED_REFERENCE` slot identity +without promoting the compatibility `refCount`. The identity transfers when +the element is overwritten and retires on container clearing or scalar cleanup; +weaken/unweaken updates the same ownership record. `UntrackedReferenceOwnerSlotTest` +covers `-1` and `WEAKLY_TRACKED` referents and verifies their legacy counts stay +unchanged. The full default-off `nice -n 19 make` gate passes. With corrective +sweeps disabled, explicit-return cleanup passes 6/6, nested-form lifetime +passes 29/29, HTML-Tree 5.07 `t/refloop.t` passes 8/8, and representative +weaken, DESTROY, cycle, and resurrection tests pass on both backends. + +### Durable scalar-cell identities for untracked values (2026-10-28) + +Durable scalar cells now record one `UNTRACKED_REFERENCE` identity for a strong +value edge when the referent is still untracked or already weakly tracked. +This includes lexical cells, package roots, closure captures, CODE slots, and +array/hash element cells whose referents remain untracked or weakly tracked. +These identities leave legacy counts unchanged; anonymous nested aggregate +slots continue through their existing counted path. Assignment transfers the +same identity; lexical registration, replacement, scope exit, capture release, +container clearing, weaken, and unweaken refresh or retire it at their +ownership boundary. `UntrackedReferenceOwnerSlotTest` verifies the +untracked-to-weakly-tracked transfer and that a captured lexical keeps the +identity until its final owner exits. + +`nice -n 19 make` passes. With corrective sweeps disabled, both backends pass +explicit-return cleanup 6/6, nested-form lifetime 29/29, HTML-Tree 5.07 +`t/refloop.t` 8/8, `weaken.t` 4/4, `destroy.t` 15/15, blessed self-cycle 4/4, +and exception resurrection 11/11. + +### Weakening repairs counts from surviving owner slots (2026-10-28) + +When weakening exposes a transient undercount, `WeakRefRegistry` restores the +compatibility count from surviving counted owner slots before walking roots. +`WeakeningCountedOwnerSlotTest` covers the repair with zero root and +scalar-reachability queries. CODE weak-reference cleanup also needs to +distinguish live owners from compatibility pins and expired frame cells; the +next section records that migration. The full +`nice -n 19 make` gate passes. With corrective sweeps disabled, both backends +pass explicit-return cleanup 6/6, nested-form lifetime 29/29, weaken 4/4, +DESTROY 15/15, blessed self-cycle 4/4, exception resurrection 11/11, +Sub::Quote cleanup 8/8, and HTML-Tree 5.07 `t/refloop.t` 8/8. + +### Untracked referent weakening uses slot identities (2026-10-28) + +When an untracked referent has a durable `UNTRACKED_REFERENCE` owner slot, +`weaken()` now avoids the registry-wide corrective sweep and, for aggregates, +the initial root scan. Releasing its final owner queues targeted weak cleanup. +`UntrackedReferenceOwnerSlotTest` checks the no-sweep decision and final-slot cleanup +with corrective sweeps enabled. The focused owner-slot, unblessed-root, and +weak-CODE capture tests pass. `nice -n 19 make` passes; with corrective sweeps +disabled, both backends pass explicit-return cleanup 6/6, nested-form lifetime +29/29, weaken 4/4, DESTROY 15/15, blessed self-cycle 4/4, exception +resurrection 11/11, Sub::Quote cleanup 8/8, and HTML-Tree 5.07 `t/refloop.t` +8/8. + +### CODE weak-reference owners exclude compatibility pins (2026-10-28) + +Before relying on CODE owner slots, `WeakRefRegistry` retires eligible +frame-temporary scalar cells. It accepts remaining non-`CODE_SLOT` identities +as direct Perl owners. `CODE_SLOT` can represent a compatibility pin retained +after a glob is deleted; installed stash refs remain covered by the stash-root +checks. `WeakCodeOwnerSlotTest` verifies that a live lexical CODE slot skips the +scalar-root walk. The existing `subquote_defer_weak_cleanup.t` passes 8/8 on +both backends with sweeps disabled. `nice -n 19 make` passes in 8m14s. With +corrective sweeps disabled, both backends pass explicit-return cleanup 6/6, +nested-form lifetime 29/29, `weaken.t` 4/4, `destroy.t` 15/15, blessed +self-cycle 4/4, exception resurrection 11/11, Sub::Quote cleanup 8/8, and +HTML-Tree 5.07 `t/refloop.t` 8/8. + +### Package scalar roots get per-key owner identities (2026-10-28) + +Each package scalar table key now owns a distinct `PACKAGE_SCALAR_SLOT` +identity for its scalar cell. Replacing, removing, or clearing a key releases +that identity; aliases therefore keep separate identities. Ithread snapshots +rebuild the identities for their cloned scalar table. Package-rooted scalar +cells can now own untracked reference values without promoting the legacy +count. `UntrackedReferenceOwnerSlotTest` covers aliases, key removal, clone rebuild, +and cells already at the legacy destroyed sentinel. `nice -n 19 make` passes in +6m02s. With corrective sweeps disabled, both backends pass explicit-return +cleanup 6/6, nested-form lifetime 29/29, `weaken.t` 4/4, `destroy.t` 15/15, +blessed self-cycle 4/4, exception resurrection 11/11, Sub::Quote cleanup 8/8, +and HTML-Tree 5.07 `t/refloop.t` 8/8. + +### Tied handlers and staged values retain untracked referents (2026-10-28) + +Tied-handler values and staged call/return values now mark their owning scalar +cell as a durable strong slot. That lets the existing +`UNTRACKED_REFERENCE` identity represent their referents without promoting a +legacy `refCount`; release retires the identity once and defers any counted +decrement. Added owner-slot tests cover untracked values, weaken without a root +scan, and idempotent release. Runtime clone setup also tolerates rebuilding +these identities before the destination runtime is bound. The focused tied +storage and owner-slot Java tests pass, followed by the full `nice -n 19 make` +gate (6m30s). With corrective sweeps disabled, all eight lifecycle cases pass +on both backends, including HTML-Tree 5.07 `t/refloop.t` (8/8). + +### Untracked mortal return values release at caller boundaries (2026-10-28) + +Mortal return snapshots whose referents remain untracked or weakly tracked now +receive `UNTRACKED_REFERENCE` identities. Their release follows the existing +mortal mark stack, so nested frames cannot retire a caller's return value; +final owner release clears weak observers directly when corrective sweeps are +disabled. `MortalReturnUntrackedOwnerTest` passes both cases with sweeps +disabled and records zero root queries, snapshots, and scalar reachability +queries for final weak cleanup. The full `nice -n 19 make` gate passes in +6m17s. The sweep-disabled closure, weak/DESTROY, resurrection, cycle, +Sub::Quote, and HTML-Tree matrix passes on both backends; HTML-Tree +`t/refloop.t` is 8/8. + +### Package array and hash roots get per-key owner identities (2026-10-28) + +Each package array or hash key now owns a distinct `PACKAGE_AGGREGATE_SLOT`. +The global-map facade transfers or releases those identities for replacement, +key/value/entry removal, entry replacement, clear, and cloned runtime tables. +Final root release clears weak observers directly when corrective sweeps are +disabled. `SvREFCNT` keeps Perl's special `%^H` count by excluding its hidden +package-root identity. `PackageAggregateOwnerSlotTest` covers aliases, +replacement, clearing, clone reconstruction, and weak cleanup; related tests +now live in new test classes instead of modifying existing test files. +The full `nice -n 19 make` gate passes in 7m05s. With sweeps disabled, 16 +closure, weak/DESTROY, resurrection, cycle, Sub::Quote, and HTML-Tree cases +pass on both backends; HTML-Tree `t/refloop.t` is 8/8. + +### Weak CODE release uses direct owner identities (2026-10-28) + +Package-rooted array and hash cells now register their value ownership when +the aggregate is marked as a package root. `shouldKeepCodeWeakRefs` relies on +active owner slots after checking installed CODE slots, active frames, and +retiring stale frame temporaries. It no longer consults sweep-local live sets, +full-root snapshots, or scalar-root scans. `WeakCodeOwnerSlotTest` verifies +lexical and package-array CODE owners with zero reachability queries. The full +`nice -n 19 make` gate passes in 6m33s, and the 16-case sweeps-disabled +JVM/interpreter matrix passes, including HTML-Tree `t/refloop.t` 8/8. + +### Untracked container values refresh and retire owners (2026-10-28) + +Array and hash writes now refresh untracked scalar referent identities after +the element is installed. Copy construction refreshes only after the new +storage is assigned, and scope cleanup releases each identity before the +no-weak-reference fast return. Repeated cleanup leaves no stale identity. +`UntrackedReferenceOwnerSlotTest` covers raw and normal array/hash writes, +copy construction, removal, and scope exit without root or scalar-reachability +queries. Untracked array/hash referents still use the existing root proof on +their first weak transition; their native release semantics remain to be +migrated. `nice -n 19 make` passes in 9m55s. With corrective sweeps disabled, +the 16-case lifecycle matrix passes on both backends, including HTML-Tree +`t/refloop.t` 8/8. + +### Active lexical pads own aggregate cells (2026-10-28) + +Active lexical frames and top-level pads now keep owner identities for registered +array and hash cells. Lexical scope cleanup releases each pad identity after +aggregate cleanup, allowing weak observers to clear directly when corrective +sweeps are disabled. `ActiveLexicalAggregateOwnerSlotTest` covers both pad +types, and the UNITCHECK capture regression guards against releasing captured +children too early. The full `nice -n 19 make` gate passes in 8m02s. With sweeps +disabled, the UNITCHECK and lifecycle matrix passes on both backends, and +HTML-Tree 5.07 `t/refloop.t` passes 8/8 on both backends. The recursive +`new_from_lol` reduction still fails three interpreter assertions; this is the +known interpreter lifecycle gap tracked below. + +### Untracked aggregate values use explicit owner identities (2026-10-28) + +Durable scalar cells now attach `UNTRACKED_REFERENCE` identities to array and +hash referents as well as scalar referents, without promoting their legacy +counts. First weaken trusts direct uncounted slot identities, including active +lexical and package aggregate roots. Final release of a weakly-tracked aggregate +owner now performs direct destruction and cascading cleanup with sweeps disabled; +blessed aggregates run `DESTROY` before weak observers clear. The new +`UntrackedAggregateOwnerSlotTest` covers two independent scalar owners, lexical +and package roots, zero root queries, and blessed `DESTROY` ordering. The full +`nice -n 19 make` gate passes in 8m48s. With sweeps disabled, the UNITCHECK, +closure, weak/DESTROY/resurrection/cycle, Sub::Quote, and HTML-Tree `t/refloop.t` +8/8 tests pass on both backends. The recursive `new_from_lol` gap was caused by +interpreter scope cleanup emitting releases for captured outer pad registers. +`BytecodeCompiler` now excludes those registers from normal, explicit-return, +condition, and tail-call cleanup, matching the JVM backend. The +`issue_1649_recursive_builder_pad_release.t` regression passes 9/9 on both +backends with corrective sweeps disabled and on system Perl. An earlier +unfiltered `make` reported four JVM lifetime failures: +`catalyst_request_body_tempfile_lifetime.t`, +`file_temp_path_wrapper_lifetime.t`, +`inc_hook_generator_state.t`, and +`refcount/blessed_scalar_reference_contents_destroy.t`. The full no-sweeps +2026-10-29 `make` gate now passes; the AnyEvent listener test remains +environment-dependent because this host cannot allocate its loopback port. + +### Package-rooted untracked aggregate release (2026-10-29) + +When first weaken uses a package-root proof to preserve an untracked aggregate, +the temporary compatibility count now retires after its final untracked owner +slot leaves. `RuntimeScalar.undefine()` refreshes the scalar's owner identity, +and counted-slot promotion records the new owner before releasing the old one. +The new Java two-owner regression and Perl package-array/hash regression pass; +the six Perl assertions pass on system Perl and both PerlOnJava backends with +corrective sweeps disabled. The first-weaken root query now returns from the +owner-slot ledger before inspecting package edges; its regression records one +query and zero edges or nodes visited. The full no-sweeps `nice -n 19 make` +passes in 11m. HTML-Tree 5.07 `t/refloop.t` passes 8/8 on both backends, and +closure, weak, DESTROY, resurrection, and cycle regressions pass on both +backends with sweeps disabled. + +### JVM-live scalar registry is not Perl ownership (2026-10-29) + +The zero-count branch in `WeakRefRegistry.weaken` no longer uses +`isReachableFromLiveScalarRegistry` to rescue a referent. Tail-call cleanup +also stopped using that forced-GC registry scan. A Java-live wrapper in the +weak-key registry is not a Perl owner; durable scalar owners must be +represented by owner slots or another explicit Perl root. +`ScalarRegistryIsNotPerlOwnerTest` covers first-weaken cleanup, and +`TailCallScalarRegistryOwnershipTest` covers the released tail-call owner. +Async Future resume now checks the referent's counted or direct owner-slot +state, excluding temporary evaluation roots. Other sweep and reachability +callers still need to be migrated. `PerlStrongOwnerProofTest` covers counted, +untracked, and temporary owner slots; `FutureAsyncAwaitRuntimeTest` passes in +the full no-sweeps `make` gate. + +### Uncounted owner slots release without a root walk (2026-10-29) + +When weakening the final counted probe leaves an uncounted strong slot, +`WeakRefRegistry.weaken` now trusts that slot directly and transitions the +referent to `WEAKLY_TRACKED`. Releasing the final slot then clears weak +observers immediately, without a graph query or compatibility count. The new +`WeakUncountedOwnerSlotFastPathTest` checks the transition, zero root queries, +and final cleanup. The no-sweeps full `nice -n 19 make` gate passes in 10m30s. +The weak, DESTROY, resurrection, closure, and cycle matrix passes on both +backends; HTML-Tree 5.07 `t/refloop.t` passes 8/8 on both. + +### Untracked first-weaken avoids generic root fallback (2026-10-29) + +First weaken of an otherwise untracked aggregate now keeps the transitional +`WEAKLY_TRACKED` sentinel until a durable owner slot is installed, without +walking the general root graph. The narrow legacy package-root rescue remains +for aggregates already nested in a package global. Added +`UntrackedAggregateFirstWeakenFastPathTest` for the zero-query transition and +direct final-slot cleanup. The no-sweeps full `nice -n 19 make` gate passes in +10m34s; HTML-Tree 5.07 `t/refloop.t` passes 8/8 on both backends. + +### Counted scalar release trusts surviving uncounted slots (2026-10-29) + +When overwriting the last counted scalar reference leaves an uncounted owner +slot, `RuntimeScalar` now transitions a non-CODE referent to `WEAKLY_TRACKED` +directly. The final slot release then clears weak observers without a +package-root query. `RuntimeScalarUncountedOwnerRepairTest` covers the +undercount repair and final release with corrective sweeps disabled. CODE keeps +its native active-frame zero-count protocol; remaining root checks protect +owners that do not yet have slot identities. + +### Localized CODE release uses owner slots (2026-10-29) + +Restoring a localized glob now releases the displaced CODE and its captured +pads when no active frame or owner slot remains. The release path no longer +scans scalar registries or root graphs; the existing active-frame owner test +still protects executing CODE. `LocalizedGlobCodeOwnerReleaseTest` verifies +capture cleanup and zero root queries. The no-sweeps full `nice -n 19 make` +passes in 10m45s. The closure, weak-reference, DESTROY, resurrection, cycle, +localized-glob, and HTML-Tree `t/refloop.t` (8/8) matrix passes on both backends. + +### Uncounted owners defer zero-count cleanup without weak observers (2026-10-29) + +Scalar overwrite, deferred decrement, and tail-call release now recognize a +surviving uncounted aggregate owner slot before reaching root fallbacks. Final +slot release dispatches cleanup even when no weak observer exists. A separate +pending-release marker keeps this path distinct from legacy `WEAKLY_TRACKED` +values. `UncountedOwnerSlotZeroCountReleaseTest` covers all three paths and +verifies exactly one final cleanup with no root query. The scalar-overwrite +path still needs direct owner-slot coverage for scalar and IO referents. + +### Counted slots repair unblessed scalar overwrites (2026-10-29) + +`RuntimeScalar.setLargeRefCounted()` and `MortalList.processDeferredBase()` now +restore transient zeros from verified, current array/hash element slots for +unblessed referents; blessed referents keep the counted-slot path. The check +requires that the owner scalar still points at the referent and remains in its +recorded container. `CountedOwnerSlotRepairsUnblessedReferentTest` covers scalar +overwrite, and `DeferredUnblessedAggregateOwnerSlotRepairTest` covers deferred +release. Both verify no root query or snapshot and weak clearing after the final +slot leaves. Scalar-cell-only owners without container provenance still use the +root snapshot, so stale wrappers are not mistaken for live Perl roots. + +The full no-sweeps `nice -n 19 make` gate passes in 11m03s. Six lifecycle tests +pass 83/83 on both backends, and HTML-Tree 5.07 `t/refloop.t` passes 8/8 on both +backends with sweeps disabled. The existing scalar-wrapper snapshot tests also +pass in the full gate. + +### Tail-call cleanup trusts direct uncounted owner slots (2026-10-29) + +Tail-call release now defers final cleanup when a non-counted Perl owner slot +remains, including package scalar slots. Temporary runtime roots do not count as +Perl owners. `TailCallUncountedOwnerSlotFastPathTest` checks zero root queries +and cleanup after the package slot leaves. The no-sweeps `nice -n 19 make` gate +passed in 10m58s; the regression passed with no skips or failures. + +### Durable uncounted slots survive referent promotion (2026-10-29) + +An existing durable uncounted scalar slot now remains in the ledger when its +referent becomes tracked. Scalar overwrite and deferred release defer cleanup to +that slot instead of trusting a transient zero. Final release checks the exact +owning scalar and its container's Perl ownership, so a temporary argument array +cannot pin the referent after its frame ends. `PromotedUntrackedOwnerSlotTest` +covers package ownership and temporary-container cleanup. The no-sweeps full +`nice -n 19 make` gate passed in 14m59s. Blessed scalar contents, Catalyst +tempfile lifetime, closure cleanup, weak/DESTROY, exception resurrection, +weak-closure, and HTML-Tree `t/refloop.t` (8/8) passed on both backends. + +### Registered CODE owners skip a duplicate scalar query (2026-10-29) + +Void-discard cleanup now accepts registered container membership as direct +proof that a CODE scalar slot is live, without repeating the same check through +`ReachabilityWalker.isScalarReachable()`. `MortalizeRegisteredCodeOwnerFastPathTest` +asserts that cleanup performs zero scalar reachability queries and root walks. +The full `nice -n 19 make` gate passed in 21m18s with corrective sweeps disabled. + +### Captured scalar-cell owners repair transient undercounts (2026-10-29) + +Immediate scalar overwrite and deferred cleanup now restore an unblessed +referent count from its exact current scalar-cell owner when that cell has an +active capture slot whose CODE owner is still live. This avoids root walks for +captured scalar-cell ownership without treating conservative captures as Perl +owners. `CapturedScalarCellOwnerFastPathTest` covers both cleanup paths, checks +that root-query counters stay at zero, and verifies weak cleanup after the last +capture is released. The full no-sweeps `nice -n 19 make` gate passes in 14m02s. + +### Final scalar CODE owner releases captures from the owner ledger (2026-10-29) + +Overwriting the final scalar CODE owner now releases closure captures after +checking other scalar, stash, active-frame, transfer, and installed-global +owners directly. It does not trust a possibly inflated compatibility count or +walk the root graph. `FinalCodeOwnerCaptureReleaseFastPathTest` verifies the +final pad capture is released with zero root queries and snapshots. The full +no-sweeps `nice -n 19 make` gate passes in 13m16s. + +### Aggregate captures release from CODE owner identities (2026-10-29) + +Scope-exit cleanup now decides whether a captured aggregate's CODE owner is +live from owner-slot identities, ignoring a stale positive compatibility +count. Compiled regex callbacks now keep their CODE object through a typed +owner slot, so their captured pads survive until the regex releases that +callback. `ClosureAggregateOwnerIdentityReleaseTest` covers stale-count +release and retention by a scalar owner; the existing callback DESTROY test +covers callback lifetime. The legacy weak-backref graph walk remains for +captures that have no recorded owner identity. The no-sweeps full `make` gate +passed in 15m56s. The callback DESTROY test passed 3/3 on standard Perl and +both PerlOnJava backends; HTML-Tree `t/refloop.t` passed 8/8 on both backends. +The owner-slot regressions cover stale positive CODE counts and preservation by +live scalar and callback owners. + +### Non-captured scalar-cell owners repair transient undercounts (2026-10-29) + +Immediate overwrite and deferred cleanup now restore an unblessed referent +from verified current scalar-cell identities, including return, argument, and +CODE slots. The repair counts current scalar and aggregate owners together; +unrooted wrappers and capture counts without a live CODE owner do not prove +liveness. Focused regressions cover both cleanup paths, mixed owner families, +weak clearing, and zero root snapshots. The no-sweeps `nice -n 19 make` gate +passed in 10m41s. HTML-Tree 5.07 `t/refloop.t` passed 8/8 on both backends; +`make check-links` passed. + +### First-weaken package aggregate proof (2026-10-29) + +First weakening of an untracked package aggregate now validates its active +`UNTRACKED_REFERENCE` scalar owners directly, including whether each owner +still occupies a durable strong slot. This removes the root-reachability call +from that transition. The existing package-nested owner-slot tests pass as +part of the no-sweeps `nice -n 19 make` gate (11m53s), including the proof-only +query with zero graph edges and the final-owner weak-observer release. +HTML-Tree 5.07 `t/refloop.t` passes 8/8 on both backends with sweeps disabled. + +### Tail-call argument owner release without a root walk (2026-10-29) + +Tail-call argument release now finalizes from counted and uncounted owner +identities. The root-graph fallback is removed; a new regression confirms that +a Java-live scalar-registry entry cannot preserve the released Perl referent or +cause a root query. The test passes in the full no-sweeps `nice -n 19 make` +gate (13m06s); HTML-Tree 5.07 `t/refloop.t` passes 8/8 on both backends with +sweeps disabled. + +### Package scalar value slots for tracked referents (2026-10-29) + +A package scalar cell now records an `UNTRACKED_REFERENCE` identity for a +strong referent value when the cell has no counted owner slot. The identity +does not inflate the legacy count and is released with the package scalar slot. +A regression checks that an independent counted owner survives until its own +release. The full no-sweeps `nice -n 19 make` gate passes in 10m35s, and +HTML-Tree 5.07 `t/refloop.t` passes 8/8 on both backends with sweeps disabled. + +Live lexical array and hash cells with no counted owner slot also record an +`UNTRACKED_REFERENCE` identity for their current referent. The slot keeps a +tracked referent alive across another owner's release and clears weak observers +when the aggregate cell releases its final value owner. The new regression and +full no-sweeps `nice -n 19 make` gate pass (12m51s); HTML-Tree 5.07 +`t/refloop.t` passes 8/8 on both backends with sweeps disabled. + +### Package aggregate scalar-cell values (2026-10-29) + +Prepopulated package arrays and hashes now record an `UNTRACKED_REFERENCE` +identity when a value slot points to a scalar cell with no counted owner. The +new regression checks final weak clearing without root queries or snapshots on +both aggregate types. The full no-sweeps `nice -n 19 make` gate passes in 6m53s. +Package aggregate edges to array/hash referents and cross-container aliases +remain on the root-snapshot path. + +### Prepopulated package aggregate values that reference aggregates (2026-10-29) + +When a package array or hash becomes rooted with existing values, its scalar +cells now record an `UNTRACKED_REFERENCE` identity for array/hash referents. +Final value replacement clears weak observers without root queries or +snapshots. The identity is limited to cells present when the package aggregate +becomes a root; values inserted later and cross-container aliases retain the +root-snapshot fallback until their storage provenance is explicit. The new +array/hash regressions and full no-sweeps `nice -n 19 make` gate pass (9m30s). + +### Caller arguments preserve source scalar identity (2026-10-30) + +`caller()` now populates `@DB::args` with the selected frame's existing scalar +cells instead of copied wrappers. Writes through the debugger array therefore +reach the caller's variable, and source-cell cleanup releases the same owner +slot rather than leaving a detached untracked reference behind. Aliased package +arrays no longer promote their borrowed cells to package roots. + +The new `caller_db_args_alias_slot.t` regression passes under system Perl and +both PerlOnJava backends; it fails on the preceding JAR by leaving the caller's +value unchanged. The existing tied-args, freed-argument, DESTROY-capture, +zombie-reference, and failed-constructor weak-cache tests pass on system Perl +and both backends. The full no-sweeps `nice -n 19 make` gate passes, and +HTML-Tree 5.07 `t/refloop.t` passes 8/8 on both backends. + +### Undef releases one weakly tracked owner at a time (2026-10-30) + +`RuntimeScalar.undefine()` now checks the remaining Perl owner identities after +retiring the current cell's untracked owner. A weakly tracked referent stays live +while another owner slot remains; the final slot release clears weak observers +and performs cleanup. This fixes the unconditional cleanup in `undefine()` +instead of relying on a positive legacy count to hide that path. +`WeaklyTrackedUndefOwnerSlotTest` covers two independent lexical cells, and +`weakly_tracked_undef_preserves_other_owner.t` records the standard Perl +behavior. Both backend runs pass with corrective sweeps disabled. The no-sweeps +full `nice -n 19 make` gate passes in 11m55s, and HTML-Tree 5.07 `t/refloop.t` +passes 8/8 on both backends. + +### Package-nested aggregate cleanup uses owner identities (2026-10-30) + +The package-root compatibility count remains required by the existing +root-proof contract, but it now has a dedicated owner-slot identity. The +package element scalar slots remain the Perl ownership evidence; when their final slot +leaves, cleanup retires the compatibility identity and count before clearing +the weak observer. This makes the transitional pin explicit and exactly-once +instead of leaving it as an untracked positive count. A focused Java regression +checks pin identity, final-slot release, idempotence, and unweakened count +preservation; a Perl-level test records the package array lifetime oracle. The +root proof validates direct scalar-owner identities without traversing package +edges. + +### Interpreter return ownership precedes aggregate cleanup (2026-10-30) + +The interpreter previously prepared an explicit return value from scalar +cleanup opcodes. A subroutine with only an array or hash lexical skipped those +opcodes, so aggregate cleanup could release a returned closure's captures +before the closure acquired its return owner. The bytecode now prepares the +return owner once before any scalar, array, or hash cleanup, matching the JVM +backend's ordering. The existing `closure_capture_explicit_return.t` test +reproduces the interpreter failure and passes 6/6 on standard Perl, the JVM, +and the interpreter after the change, with corrective sweeps disabled. The +full no-sweeps `nice -n 19 make` gate passes in 12m47s. The related closure, +weak-reference, DESTROY, resurrection, and cycle matrix passes on both +backends; HTML-Tree 5.07 `t/refloop.t` passes 8/8 on both. + +### Growing recursive output avoids repeated live-root scans (2026-10-30) + +Added `issue_1860_growing_lexical_output.t`, a project-owned reproduction of +Data::Tumbler's recursive path/value/payload results accumulating in a lexical +output array. It passes 4/4 on system Perl and both PerlOnJava backends with +corrective sweeps disabled. `GrowingLexicalOutputOwnerSlotCostTest` grows a +256-row nested output graph while running the corresponding temporary-array +cleanup; its measured work is zero root queries, live/external snapshots, and +edges inspected, while weak observers stay live until the final output owner +is removed. The full no-sweeps `nice -n 19 make` gate passes in 12m39s. +Unchanged Data-Tumbler 0.010 `t/90-stress.t` generates all 65,536 results in +7.38s on the JVM backend and 11.20s on the interpreter; system Perl completes +it in 0.15s. + +### Mojolicious controller teardown follows callback owners (2026-10-30) + +Added focused #1794 coverage for controller destruction after a long-poll +response finishes, controller retention while a WebSocket is open, and release +of a controller captured by transaction callbacks. The three project tests pass +14/14 on system Perl and both PerlOnJava backends with corrective sweeps +disabled. `NestedUntrackedOwnerFinalCellCleanupTest` also passes 1/1: retiring +the nested cell slot preserves an independently counted owner and its weak +observer, which clears when that counted owner leaves. The no-sweeps +`nice -n 19 make test-unit` gate passes in 12m24s. Mojolicious 9.53 +`t/mojolicious/app.t` passes 459/459 and +`t/mojolicious/websocket_lite_app.t` passes 35/35 on all three runtimes. The +pre-fix baseline reproduces the controller teardown failure in both backends; +the WebSocket server-push suite also passes on that baseline, so the added +WebSocket case remains acceptance coverage rather than a reproduced failure. +The full `nice -n 19 make` gate still needs to run after this ownership batch is +complete. + +### Weak CODE capture walk removed from quiet sweep decisions (2026-10-30) + +Quiet weak-reference cleanup no longer builds `WeakBackrefCaptureSnapshot` to +preserve aggregate candidates by traversing captures from weak CODE referents. +Real closure captures already carry `CLOSURE_PAD`, `ARRAY_ELEMENT`, and +`HASH_ELEMENT` owner identities; the regular issue-1649 aggregate lifecycle +tests pass with corrective sweeps disabled on both backends. The weakly held +closure oracle also clears both CODE and captured target on system Perl and +both backends. Full `nice -n 19 make` passes in 7m59s. + +The snapshot helper remains reachable only through the post-DESTROY diagnostic +path in `MortalList.flush`; `WeakBackrefCodeCaptureSweepTest` currently asserts +those diagnostic walk counters. The production corrective sweep constant is +already disabled, so this gate compiled the removal but did not execute that +legacy sweep branch. Remove the remaining diagnostic walk when the test no +longer requires its internal counters. + +### Weakening retires stale untracked owner slots (2026-10-30) + +When the last counted owner is weakened, `WeakRefRegistry.weaken()` now +refreshes `UNTRACKED_REFERENCE` slots against their scalar cell's current +durable storage before preserving the referent. A cell whose container has +lost its Perl owner can no longer leave a stale slot behind and strand the weak +observer until a later sweep. `WeakeningPrunesStaleUntrackedOwnerSlotTest` +checks final weak clearing with zero root queries or snapshots. The full +`nice -n 19 make` gate passes in 8m08s; issue-1649 aggregate and closure-owner +tests pass on both backends, as does HTML-Tree 5.07 `t/refloop.t` (8/8 each). + +### Interpreter pad constants follow the published CV owner (2026-10-31) + +Lazy interpreter materialization left optree constant owner slots on the +temporary `InterpretedCode` wrapper, while the stable placeholder is the +Perl-owned CV. Transfer the direct and embedded pad-constant tree to that +placeholder on both the normal interpreter path and the VerifyError fallback. +The stale-positive-count cleanup check seeds a positive `refCount` in the Java +test to exercise this boundary; it is an invariant test, not evidence that this +scenario naturally leaves a positive count after the last owner exits. In +production, cleanup uses the remaining owner identities instead of treating the +compatibility count as another owner. The +`pad_constant_cv_owner_lifetime.t` oracle passes 10/10 on system Perl and both +backends with corrective sweeps disabled; the new Java owner-slot regression +also checks final pad-tree release with zero root queries or snapshots. The +full `nice -n 19 make` gate passes in 7m27s. HTML-Tree 5.07 `t/refloop.t` +passes 8/8 on all three runtimes. The #1860 growing-output regression and +Data-Tumbler 0.010 `t/90-stress.t` pass on system Perl and both no-sweeps +backends; the stress case generates 65,536 results in 6.56s on JVM and 10.76s +on the interpreter. The #1794 controller and WebSocket regressions pass on +system Perl and both no-sweeps backends. + +### Full upstream HTML::Tree 5.07 suite (2026-10-31) + +Ran all 23 test files from the [upstream HTML-Tree 5.07 distribution](https://metacpan.org/dist/HTML-Tree), +not just the previously extracted `t/refloop.t` fixture. System Perl passes +969 tests; `t/leaktest.t` skips because `Test::LeakTrace` is unavailable. +JVM and interpreter each pass all 23 files with corrective sweeps disabled +(988 TAP checks per backend). The bundled PerlOnJava `Test::LeakTrace` module +is a no-op stub, so its 20 `t/leaktest.t` checks are not leak measurements. +The target `t/refloop.t` remains 8/8 on system Perl and both backends. Logs: +`/private/tmp/html-tree-full-system-perl.log`, +`/private/tmp/html-tree-full-jvm-gate.log`, and +`/private/tmp/html-tree-full-interpreter-gate.log`. + +### Package-root nested weak-owner oracle (2026-10-31) + +Added `src/test/resources/unit/refcount/issue_1649_package_nested_untracked_weak_owner.t` +to exercise an untracked aggregate retained only by a package array, then release +that owner while checking weak-observer cleanup. The second scenario verifies +that `unweaken` creates a strong owner that survives package-array removal and +releases the weak probe only when that scalar is dropped. The test passes 7/7 on +system Perl and both PerlOnJava backends with corrective sweeps disabled; the +full `nice -n 19 make` gate passes in 6m55s with the test included. This records +the observable contract while the package-root compatibility pin remains a +migration bridge; it does not claim the bridge has been removed. + +### Cleared aggregate cleanup ignores legacy count as sole owner proof (2026-10-31) + +The cleared-aggregate scope-exit finalizer now checks current Perl owner +identities independently of `refCount`. A positive legacy count alone no longer +blocks final weak cleanup; temporary roots and the cached external-root proof +still preserve owners at boundaries that have not migrated. The regression +releases a nested untracked scalar owner, leaves a positive compatibility count, +and verifies the weak observer is cleared. The full `nice -n 19 make` gate passes +in 7m17s with the regression included. After this change, the full upstream +HTML-Tree 5.07 suite passes all 23 files and 989 TAP checks on both backends +with corrective sweeps disabled; `t/refloop.t` passes 8/8. The bundled +`Test::LeakTrace` stub makes HTML-Tree's 20 leak-test checks non-measurements. +The #1794 controller and WebSocket regressions pass 3/3 and 6/6 respectively +on both backends, and the #1860 growing-output regression passes 4/4 on both. +A focused post-change weak/DESTROY/resurrection/cycle matrix also passes all +8 fixtures and 68 assertions on each backend. + +The count of `5` in `finalCellCleanupReleasesNestedUntrackedOwnerDespiteLegacyCount` +is assigned directly by the Java test; it is an invariant check, not evidence +that normal runtime operations create that state. A Perl-level async reproducer +did expose a real ordering window: scope-exit finalization can run after a +counted owner slot is retired but before its queued decrement drains. RuntimeBase +now tracks queued owner releases through completion, and cleared-aggregate +finalization leaves those releases to decide the lifecycle transition. A focused +Java regression exercises this real queue path and passes in the full +`nice -n 19 make` gate. A Perl-level async case passes 2/2 checks on both +PerlOnJava backends; the equivalent standard-Perl lifetime check passes 2/2. + +### Next steps + +1. Remove production corrective-sweep scheduling as each strong-owner family + becomes identity-complete. Keep explicit diagnostic sweep helpers isolated + from production lifecycle paths while existing helper tests still exercise + them. +2. In one ownership-family batch, finish remaining CODE template and optree + pins, raw descriptor/embedding borrows, native call buffers, escaping + lvalues and refaliases, and targeted try/finally returns. Add regressions + before running the focused gates and one full `nice -n 19 make`. +3. Requalify the full upstream HTML-Tree suite, weak/DESTROY, resurrection, + cycle, and repository suite after each ownership-family migration. +4. Make semantic owner counts authoritative, then remove the compatibility + bridge, production corrective scheduling, and reachability fallbacks after + all acceptance gates pass. diff --git a/dev/design/refcount-owner-ledger.md b/dev/design/refcount-owner-ledger.md index bf6907a7d4..9a6c74daba 100644 --- a/dev/design/refcount-owner-ledger.md +++ b/dev/design/refcount-owner-ledger.md @@ -371,7 +371,7 @@ Run on JVM and interpreter backends with `timeout` and complete output logs: ## Progress Tracking -### Current Status: Net::Async::HTTP captured-owner, hash-slice, and eval-capture finalization fixes complete +### Current Status: sweep scheduling removed; owner-source migration continues The designated issue branch, `fix/issue-1132-closure-lifetime`, includes the handoff commits below. On 2026-09-01, the required pre-change `make` rebuilt @@ -529,6 +529,78 @@ external workload has drained before changing runtime source. in `perl5_t/t/run/fresh_perl.t` test 75 now passes, restoring the baseline 73/91 result; the other 18 historical failures are unchanged. Full `make` passed in the isolated PR worktree on 2026-09-02. +- [x] On 2026-10-28, removed automatic, targeted, and statement-boundary weak + cleanup scheduling from `MortalList`; legacy request methods remain no-op + compatibility hooks. Added direct cleanup for an untracked value when its + final scalar-cell owner exits, and kept reachability fallbacks where slot + coverage is incomplete. `nice -n 19 make` passed with + `JPERL_DISABLE_CORRECTIVE_SWEEPS=1` and Gradle limited to two workers. +- [x] On 2026-10-28, CODE weak-reference registration stopped walking scalar + roots to decide whether an anonymous CODE value still has an owner. The + existing stash, active-frame, and owner-slot checks in `clearWeakRefsTo()` + now make that decision. Added + `WeakCodeOwnerSlotNoReachabilityTest`; the full `make` gate passed with + corrective sweeps disabled. +- [x] On 2026-10-29, package-rooted untracked aggregates now retire the + temporary count rescued at first weaken when their final untracked owner + slot leaves. Added Java owner-release coverage and six Perl assertions for + package-array clear and package-hash delete; system Perl, both backends, + the full no-sweeps `make`, and HTML-Tree 5.07 `t/refloop.t` (8/8 on both + backends) passed. The first-weaken root-query entry now returns from the + owner-slot ledger before inspecting package edges; its regression sees zero + edges and nodes visited. +- [x] On 2026-10-29, tracked zero-count weakening now trusts surviving + uncounted owner slots directly and enters `WEAKLY_TRACKED`; final slot release + clears weak observers without a root walk or compatibility pin. + `WeakUncountedOwnerSlotFastPathTest`, the no-sweeps full `make`, and the weak, + DESTROY, resurrection, closure, cycle, and HTML-Tree 8/8 matrix pass. +- [x] On 2026-10-29, first weaken of an untracked aggregate now keeps a + transitional `WEAKLY_TRACKED` sentinel without the generic root-graph + fallback; its durable slot release clears the weak observer directly. + `UntrackedAggregateFirstWeakenFastPathTest`, the no-sweeps full `make`, and + HTML-Tree 5.07 `t/refloop.t` (8/8 on both backends) pass. The package-root + rescue proof remains for the legacy nested-aggregate case. +- [x] On 2026-10-29, a surviving uncounted owner slot now preserves weak + observers when `RuntimeScalar` releases the last counted slot. The final + owner release performs direct cleanup without a root query; permanent + regression coverage is in `RuntimeScalarUncountedOwnerRepairTest`. +- [x] On 2026-10-29, localized CODE restoration now releases displaced CODE + captures from active-frame and owner-slot identities, without scalar-registry + or root-graph scans. `LocalizedGlobCodeOwnerReleaseTest` covers final capture + cleanup; `LocalizedGlobCodeOwnerSlotFastPathTest` covers a live active call. + The no-sweeps full `make`, closure/weak/DESTROY/resurrection/cycle matrix, and + HTML-Tree 5.07 `t/refloop.t` (8/8) pass on both backends. +- [x] On 2026-10-29, scalar overwrite, deferred decrement, and tail-call release + now defer zero-count cleanup for aggregates with a surviving uncounted owner + slot, even with no weak observer. `UncountedOwnerSlotZeroCountReleaseTest` + verifies each path and final destruction after the sole owner slot leaves. + Scalar and IO referents still need owner-slot release coverage before this + rule can safely apply to them. +- [x] On 2026-10-29, tail-call zero-count release no longer treats a + Java-live scalar-registry wrapper as a Perl owner or forces GC to inspect it. + `TailCallScalarRegistryOwnershipTest` verifies that a released tail-call + referent is destroyed while its scalar wrapper remains Java-live. +- [x] On 2026-10-29, tail-call release now trusts any direct non-counted Perl + owner slot, excluding temporary runtime roots. Added + `TailCallUncountedOwnerSlotFastPathTest` for package scalar ownership; it + passes in the full no-sweeps `make` gate (10m58s). +- [x] On 2026-10-29, preserve live uncounted scalar owner slots through + referent promotion, while pruning slots in temporary containers without a + Perl owner at final release. `PromotedUntrackedOwnerSlotTest` covers both + cases. The no-sweeps full `make` gate passed in 14m59s; tempfile, closure, + weak/DESTROY, resurrection, weak-cycle, and HTML-Tree 8/8 checks passed on + both backends. +- [x] On 2026-10-29, async Future resume now checks counted and direct owner + slots instead of rebuilding root graphs or forcing GC for a scalar-registry + lookup. Temporary evaluation roots do not count as Perl owners. + `PerlStrongOwnerProofTest`, the full no-sweeps `make`, the closure/weak/ + DESTROY/resurrection/cycle matrix, and HTML-Tree `t/refloop.t` (8/8 on both + backends) pass. +- [x] On 2026-10-29, void-discard cleanup now uses registered container + membership directly as proof that a CODE scalar slot is live, avoiding a + duplicate scalar reachability query. `MortalizeRegisteredCodeOwnerFastPathTest` + verifies zero scalar queries and root walks; the no-sweeps full `make` passed + in 21m18s. ### Handoff: issue #1132 / PR #1204 @@ -546,9 +618,16 @@ pass. Preserve both focused ownership regressions when evolving the ledger. ### Next Steps -1. Continue the owner-source migration inventory and remove only diagnosed - ownership heuristics with permanent system-Perl-validated regressions. -2. Keep Cookie2 formatting and content-coding exception handling separate from +1. Audit remaining `RuntimeScalar` and `MortalList` reachability fallbacks; add + explicit slot identities where ownership is missing, then remove each + fallback only after system Perl and both backends pass. +2. Remove the legacy package-root rescue pin after every final-slot release + path has direct coverage. +3. Migrate real weak CODE captures to capture-owner identities while retaining + compatibility behavior for unrecorded legacy captures. +4. Requalify HTML-Tree's 8/8 and the full suite after remaining owner changes; + remove compatibility APIs after all gates pass. +5. Keep Cookie2 formatting and content-coding exception handling separate from this ownership work. ### Open Questions diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 0404c5f142..4129730935 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -6,7 +6,34 @@ priorities and future plans. ## Work in progress +- Disable corrective sweeps by default while migrating to explicit reference ownership; retain temporary scalar call and mortal return values through their caller boundaries, and balance argument, scalar-cell, closure, aggregate, package scalar/array/hash roots, tied-handler, shared-view, named CODE, persistent state, and destructor lifetimes (#1649). +- Use live scalar-owner slots to preserve nested untracked aggregates on first weaken, avoid package-root walks, and clear weak observers at final release (#1649). +- Release tail-call arguments from explicit owner slots without a root-graph fallback (#1649). +- Record uncounted value ownership for tracked referents held in package scalar cells (#1649). +- Give borrowed scalar cells in live lexical aggregates owner identities for their strong values (#1649). +- Preserve non-CODE weak referents directly from surviving uncounted owner slots when a scalar releases its last counted slot (#1649). +- Release displaced localized CODE captures from active-frame and owner-slot identities, without scalar-registry or root-graph scans (#1649). +- Defer zero-count cleanup for aggregates with surviving owner slots across scalar, deferred, and tail-call releases (#1649). +- Preserve live uncounted scalar owner slots through referent promotion, and prune temporary-container slots at final release (#1649). +- Avoid duplicate scalar reachability queries when a registered container proves a live CODE owner (#1649). +- Use direct non-counted Perl owner slots to defer tail-call cleanup without a root walk; ignore temporary runtime roots (#1649). +- Repair transient unblessed referent undercounts from verified array/hash element slots during overwrite and deferred cleanup, avoiding root walks (#1649). +- Repair unblessed referent undercounts from captured scalar-cell owners only while a live closure owns the cell, in overwrite and deferred cleanup (#1649). +- Release captures from a final scalar CODE owner using explicit owner identities instead of a root walk (#1649). +- Release aggregate closure captures at scope exit from CODE owner identities, ignoring stale compatibility counts (#1649). +- Track compiled regex callback CODE references with owner slots and release captures after the final callback owner leaves (#1649). +- Repair unblessed referent undercounts from verified scalar and aggregate owner slots in immediate and deferred cleanup, without root snapshots (#1649). +- Remove live-scalar-registry reachability from first-weaken, tail-call, and async-Future owner decisions. +- Release weakly tracked referents from their final uncounted owner slot without a root walk. - Keep nested Rose::HTML::Objects form results alive with counted argument owners so weak parent links and qualified field names survive argument evaluation (#1851). +- Preserve caller argument identity in @DB::args so borrowed debugger slots share source-cell cleanup instead of creating detached untracked references (#1649). +- Keep weakly tracked referents alive while an explicit Perl owner slot remains; undef now retires only the released cell's identity (#1649). +- Track package-root weak compatibility pins by owner-slot identity and retire each pin with the final scalar owner (#1649). +- Finalize cleared aggregate weak references from live owner identities even when a legacy count remains; retain reachability checks only for unmigrated owner cases (#1649). +- Keep queued owner releases visible to cleared-aggregate cleanup until their deferred count transition completes (#1649). +- Keep backslash-addressed optree constants alive through saved CODE references, transfer interpreter optree owner trees to published CVs, and release each constant edge with its CV owner (#1649). +- Acquire interpreter return owners before lexical cleanup so returned closures retain captured aggregates (#1649). +- Keep recursive output cleanup on explicit owner slots as lexical result graphs grow, avoiding repeated live-root snapshots (#1860). - Warn for undefined right operands of `.=` while keeping an undefined left operand quiet (#1382). - Fix bundled IO, nested launcher, locale, and core user/group database module compatibility. - Snapshot mutable array entries while marking package roots and during reachability walks so concurrent thread teardown cannot fail with `ConcurrentModificationException`. diff --git a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java index a957afd089..2c4e598531 100644 --- a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java +++ b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java @@ -981,6 +981,16 @@ private static RuntimeCode compileToExecutable(Node ast, EmitterContext ctx) thr generatedClass, ctx ); + RuntimeBase[] padConstants = ctx.javaClassInfo.padConstants == null + ? new RuntimeBase[0] + : ctx.javaClassInfo.padConstants.toArray(new RuntimeBase[0]); + if (padConstants.length != 0 || !ctx.javaClassInfo.embeddedPadConstants.isEmpty()) { + compiled.installPadConstantTree( + ctx.javaClassInfo.javaClassName, + padConstants, + ctx.javaClassInfo.embeddedPadConstants, + ctx.javaClassInfo.embeddedPadConstantDescendants); + } // File-scope `our` declarations, for PadWalker::peek_our(0) at file scope Map ourDeclarations = VariableCollectorVisitor.declaredOurVariables(ast); compiled.declaredOurVariables = ourDeclarations.isEmpty() ? null : ourDeclarations; diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 64db38185f..29fd032661 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -525,6 +525,8 @@ boolean isCompilingForeachList() { final String sourceName; final int sourceLine; private final List constants = new ArrayList<>(16); + /** Read-only scalar literals addressed by backslash in this optree. */ + private final List padConstants = new ArrayList<>(); private final List stringPool = new ArrayList<>(16); private final Map stringPoolIndex = new HashMap<>(16); // O(1) lookup private final Map constantPoolIndex = new HashMap<>(16); // O(1) lookup @@ -601,6 +603,10 @@ private void emitNormalLoopResult() { // Track current calling context for subroutine calls int currentCallContext = RuntimeContextType.LIST; // Default to LIST Map capturedVarIndices; // Name → register index + // Register identities for actual captured pad cells. capturedVarIndices also + // includes synthetic reservations for referenced names, so it cannot decide + // which lexical slots scope cleanup must leave alone. + private final Set capturedPadRegisterIndices = new HashSet<>(); // Declaration metadata for captured package variables. Keep this // separately from the register table because closure analysis may first // materialize a reference as a lexical before the body reaches `local`. @@ -751,6 +757,7 @@ public BytecodeCompiler(String sourceName, int sourceLine, ErrorMessageUtil erro int regIndex = entry.getValue(); if (regIndex >= 3) { capturedVarIndices.put(varName, regIndex); + capturedPadRegisterIndices.add(regIndex); } } @@ -1044,12 +1051,20 @@ private void exitScope(boolean flush) { } } + int separateReturnRegister(int sourceReg) { + int resultReg = allocateRegister(); + emit(Opcodes.ALIAS); + emitReg(resultReg); + emitReg(sourceReg); + return resultReg; + } + private void emitScopeCleanup(int scopeIdx, boolean flush) { // Gather variable indices to determine if cleanup is needed. - java.util.List scalarIndices = symbolTable.getMyScalarIndicesInScope(scopeIdx); + java.util.List scalarIndices = myScalarCleanupIndicesInScope(scopeIdx); java.util.Map scalarNames = symbolTable.getMyScalarNamesInScope(scopeIdx); - java.util.List hashIndices = symbolTable.getMyHashIndicesInScope(scopeIdx); - java.util.List arrayIndices = symbolTable.getMyArrayIndicesInScope(scopeIdx); + java.util.List hashIndices = myHashCleanupIndicesInScope(scopeIdx); + java.util.List arrayIndices = myArrayCleanupIndicesInScope(scopeIdx); // Only emit pushMark/popAndFlush when there are variables that need cleanup. // Scopes with no my-variables skip this entirely. @@ -1070,6 +1085,14 @@ private void emitScopeCleanup(int scopeIdx, boolean flush) { // implicit `($left, $right)` return closes both socketpair ends before // the caller receives them. boolean preserveImplicitReturn = scopeIndices.isEmpty() && lastResultReg >= 0; + if (preserveImplicitReturn) { + // Return materialization replaces this register with an owned RV + // snapshot. Keep it separate from every lexical register, since + // an earlier local's cleanup can prepare a later local's value. + lastResultReg = separateReturnRegister(lastResultReg); + emit(Opcodes.PREPARE_SCOPE_RETURN); + emitReg(lastResultReg); + } // Emit SCOPE_EXIT_CLEANUP for each my-scalar register in the exiting scope. // This calls RuntimeScalar.scopeExitCleanup() which handles: @@ -1109,6 +1132,39 @@ private void emitScopeCleanup(int scopeIdx, boolean flush) { } } + private boolean isCapturedVariableIndex(int index) { + return capturedPadRegisterIndices.contains(index); + } + + private List withoutCapturedVariables(List indices) { + if (indices.isEmpty() || capturedVarIndices == null || capturedVarIndices.isEmpty()) { + return indices; + } + List filtered = new ArrayList<>(indices.size()); + for (int index : indices) { + if (!isCapturedVariableIndex(index)) { + filtered.add(index); + } + } + return filtered; + } + + List myScalarCleanupIndicesInScope(int scopeIdx) { + return withoutCapturedVariables(symbolTable.getMyScalarIndicesInScope(scopeIdx)); + } + + List myHashCleanupIndicesInScope(int scopeIdx) { + return withoutCapturedVariables(symbolTable.getMyHashIndicesInScope(scopeIdx)); + } + + List myArrayCleanupIndicesInScope(int scopeIdx) { + return withoutCapturedVariables(symbolTable.getMyArrayIndicesInScope(scopeIdx)); + } + + List myVariableCleanupIndicesInScope(int scopeIdx) { + return withoutCapturedVariables(symbolTable.getMyVariableIndicesInScope(scopeIdx)); + } + private void emitLoopControlScopeCleanup(LoopInfo loopInfo) { if (loopInfo.dynamicLocalLevelReg >= 0) { emit(Opcodes.POP_LOCAL_LEVEL); @@ -1145,10 +1201,10 @@ private List indicesAddedSince(List indices, Set befo private MyVarCleanupRegisters myVariablesAddedSince(Set before) { return new MyVarCleanupRegisters( - indicesAddedSince(symbolTable.getMyScalarIndicesInScope(0), before), - indicesAddedSince(symbolTable.getMyHashIndicesInScope(0), before), - indicesAddedSince(symbolTable.getMyArrayIndicesInScope(0), before), - indicesAddedSince(symbolTable.getMyVariableIndicesInScope(0), before)); + indicesAddedSince(myScalarCleanupIndicesInScope(0), before), + indicesAddedSince(myHashCleanupIndicesInScope(0), before), + indicesAddedSince(myArrayCleanupIndicesInScope(0), before), + indicesAddedSince(myVariableCleanupIndicesInScope(0), before)); } private void emitMyVarCleanup(MyVarCleanupRegisters cleanup, boolean flush) { @@ -1635,6 +1691,18 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { evalSitePragmaFlags.isEmpty() ? null : evalSitePragmaFlags, warningBitsString ); + List ownedPadConstants = new ArrayList<>(padConstants); + for (Object constant : constants) { + RuntimeCode nestedCode = constant instanceof RuntimeCode codeConstant + ? codeConstant + : constant instanceof RuntimeScalar codeRef + && codeRef.value instanceof RuntimeCode codeValue + ? codeValue : null; + if (nestedCode != null) { + Collections.addAll(ownedPadConstants, nestedCode.padConstantOwnerReferents()); + } + } + code.installPadConstants(ownedPadConstants.toArray(new RuntimeBase[0])); if (metadataScope != null) { code.setLexicalDisabledWarningCategories( metadataScope.getDisabledWarningCategories()); @@ -1746,6 +1814,7 @@ private void detectClosureVariables(Node ast, EmitterContext ctx) { // by the closure body. if (usedVars != null && !usedVars.contains(name)) continue; capturedVarIndices.put(name, reg); + capturedPadRegisterIndices.add(reg); outerVarNames.add(name); outerVarDecls.add(entry.decl()); outerVarPackages.add(entry.perlPackage()); @@ -3968,13 +4037,10 @@ void compileVariableDeclaration(OperatorNode node, String op) { } emit(Opcodes.REGISTER_MY_VAR); emitReg(reg); - if (sigil.equals("$")) { - // Keep the active CV pad visible to deferred format - // argument evaluation. Format declarations can occur - // after a write in the source while Perl still binds - // the format to this invocation's lexical cell. - emitActiveLexicalBinding(reg, varName); - } + // Keep the active CV pad visible to runtime lexical lookup. + // Deferred format argument evaluation can also refer to + // scalar cells declared earlier in the invocation. + emitActiveLexicalBinding(reg, varName); // Runtime attribute dispatch for my variables with attributes emitVarAttrsIfNeeded(node, reg, sigil); @@ -6019,13 +6085,13 @@ void compileVariableReference(OperatorNode node, String op) { int rd = allocateOutputRegister(); if (isStrictRefsEnabled()) { // strict refs: scalarDeref() — throws for non-refs - emitWithToken(Opcodes.DEREF_SCALAR_STRICT, node.getIndex()); + emitWithToken(Opcodes.DEREF_SCALAR_STRICT_TEMP, node.getIndex()); emitReg(rd); emitReg(blockResultReg); } else { // no strict refs: scalarDerefNonStrict(pkg) — allows symrefs int pkgIdx = addToStringPool(getCurrentPackage()); - emitWithToken(Opcodes.DEREF_SCALAR_NONSTRICT, node.getIndex()); + emitWithToken(Opcodes.DEREF_SCALAR_NONSTRICT_TEMP, node.getIndex()); emitReg(rd); emitReg(blockResultReg); emit(pkgIdx); @@ -6550,6 +6616,7 @@ void compileVariableReference(OperatorNode node, String op) { emit(Opcodes.LOAD_CONST); emitReg(valueReg); emit(addToConstantPool(constant)); + padConstants.add(constant); if (emitterContext != null && emitterContext.javaClassInfo != null) { emitterContext.javaClassInfo.addPadConstant(constant); } @@ -8954,6 +9021,23 @@ public void visit(IfNode node) { patchIntOffset(gotoEndPos + 1, endPos); lastResultReg = resultReg; } + boolean conditionOwnsLocals = !myScalarCleanupIndicesInScope(ifScopeIndex).isEmpty() + || !myHashCleanupIndicesInScope(ifScopeIndex).isEmpty() + || !myArrayCleanupIndicesInScope(ifScopeIndex).isEmpty(); + if (conditionOwnsLocals) { + if (lastResultReg >= 0 && currentCallContext != RuntimeContextType.VOID) { + // A conditional can yield its declaring lexical, including a + // false overloaded reference or a reference to a local hash. + // Acquire the expression's own owners before retiring that scope. + int sourceReg = lastResultReg; + lastResultReg = allocateRegister(); + emit(Opcodes.SNAPSHOT_SCOPE_RESULT); + emitReg(lastResultReg); + emitReg(sourceReg); + emit(currentCallContext); + } + emitScopeCleanup(ifScopeIndex, true); + } symbolTable.exitScope(ifScopeIndex); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 9de201e79b..13997f34b9 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -108,31 +108,15 @@ private static boolean lexicalAssignmentMustPreserveSlot(RuntimeBase val) { || scalar.type == RuntimeScalarType.READONLY_SCALAR; } - private static boolean returnListContainsTrackedReference(RuntimeList retList) { - if (retList == null || retList instanceof RuntimeControlFlowList) return true; - for (RuntimeBase elem : retList.elements) { - if (runtimeBaseIsTrackedReference(elem)) { - return true; - } - } - return false; - } - - private static boolean runtimeBaseIsTrackedReference(RuntimeBase elem) { - if (elem == null) return false; - if (elem instanceof RuntimeScalar scalar) { - if (!RuntimeScalarType.isReference(scalar)) { - return false; - } - if (!(scalar.value instanceof RuntimeBase base)) { + private static boolean releaseConsumedTemp(RuntimeBase temp, RuntimeBase target) { + if (temp instanceof RuntimeList list) { + if (list.elements.size() != 1 || !(list.elements.getFirst() instanceof RuntimeScalar scalar)) { return false; } - return base.blessId != 0 || base.refCount >= 0; + boolean released = scalar == target || releaseConsumedTemp(scalar, target); + if (released) list.elements.clear(); + return released; } - return elem.blessId != 0 || elem.refCount >= 0; - } - - private static void releaseConsumedTemp(RuntimeBase temp, RuntimeBase target) { if (!(temp instanceof RuntimeScalar tempScalar) || !(target instanceof RuntimeScalar targetScalar) || tempScalar == targetScalar @@ -142,7 +126,7 @@ private static void releaseConsumedTemp(RuntimeBase temp, RuntimeBase target) { || tempScalar.value != targetScalar.value || !(tempScalar.value instanceof RuntimeBase base) || base.refCount <= 0) { - return; + return false; } tempScalar.refCountOwned = false; @@ -152,31 +136,46 @@ private static void releaseConsumedTemp(RuntimeBase temp, RuntimeBase target) { } base.releaseActiveOwner(tempScalar); base.refCount--; + return true; } private static java.util.Set collectReturnedClosures(RuntimeBase value) { java.util.IdentityHashMap closures = new java.util.IdentityHashMap<>(); - collectReturnedClosures(value, closures); + java.util.IdentityHashMap visited = new java.util.IdentityHashMap<>(); + collectReturnedClosures(value, closures, visited); return closures.isEmpty() ? null : closures.keySet(); } - private static void collectReturnedClosures(RuntimeBase value, java.util.IdentityHashMap closures) { - if (value == null) { - return; - } + private static void collectReturnedClosures( + RuntimeBase value, + java.util.IdentityHashMap closures, + java.util.IdentityHashMap visited) { + if (value == null || visited.put(value, Boolean.TRUE) != null) return; if (value instanceof RuntimeScalar scalar) { if (scalar.type == RuntimeScalarType.CODE && scalar.value instanceof RuntimeCode code) { closures.put(code, Boolean.TRUE); + } else if (RuntimeScalarType.isReference(scalar) + && scalar.value instanceof RuntimeBase referent + && !WeakRefRegistry.isweak(scalar)) { + collectReturnedClosures(referent, closures, visited); } return; } if (value instanceof RuntimeControlFlowList flow && flow.returnValue != null) { - collectReturnedClosures(flow.returnValue, closures); + collectReturnedClosures(flow.returnValue, closures, visited); return; } if (value instanceof RuntimeList list) { for (RuntimeBase element : list.elements) { - collectReturnedClosures(element, closures); + collectReturnedClosures(element, closures, visited); + } + } else if (value instanceof RuntimeArray array) { + for (RuntimeScalar element : array.elements) { + collectReturnedClosures(element, closures, visited); + } + } else if (value instanceof RuntimeHash hash) { + for (RuntimeScalar element : hash.elements.values()) { + collectReturnedClosures(element, closures, visited); } } } @@ -602,6 +601,22 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { code.unbindActiveLexical(code.stringPool[nameIdx], registers[reg]); } + case Opcodes.SNAPSHOT_SCOPE_RESULT -> { + int destination = bytecode[pc++]; + int source = bytecode[pc++]; + int resultContext = bytecode[pc++]; + if (resultContext == RuntimeContextType.RUNTIME) resultContext = callContext; + registers[destination] = RuntimeCode.prepareScopeResult( + registers[source], resultContext); + } + + case Opcodes.PREPARE_SCOPE_RETURN -> { + int returnReg = bytecode[pc++]; + registers[returnReg] = RuntimeCode.prepareScopeReturn( + registers[returnReg], callContext, + !RuntimeCode.isLvalueCode(code)); + } + case Opcodes.RETURN_SCOPE_CLEANUP -> { int reg = bytecode[pc++]; int returnReg = bytecode[pc++]; @@ -746,6 +761,9 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { int returnReg = bytecode[pc++]; RuntimeBase slot = registers[reg]; if (slot instanceof RuntimeArray ra) { + registers[returnReg] = RuntimeCode.prepareLocalArrayReturn( + registers[returnReg], ra, callContext, + !RuntimeCode.isLvalueCode(code)); MortalList.scopeExitCleanupArray(ra, registers[returnReg]); } } @@ -772,7 +790,7 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { savedLocalLevel + relativeLevel); } unwindEvalMethodInvocantHolds( - evalMethodInvocantHoldDepthStack, methodInvocantHolds); + evalMethodInvocantHoldDepthStack, methodInvocantHolds, evalBaseRegStack, registers); pc = evalCatchStack.pop(); RuntimeCode.decrementEvalDepth(); popEvalCallerFrame(frame); @@ -784,10 +802,12 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { RuntimeCode.materializeSpecialVarsInResult( retList, RuntimeCode.currentRawCallContext()); frame.returnedClosures = collectReturnedClosures(retList); - if (!returnListContainsTrackedReference(retList)) { - MortalList.flushAboveMark(); - } - + // Leave frame-local decrements queued here. The + // RuntimeCode call boundary releases lexical + // registrations before flushing them; flushing + // at the opcode can still see the returning + // frame as an owner and postpone DESTROY until + // a corrective sweep. return retList; } @@ -1378,7 +1398,7 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { String name = code.stringPool[nameIdx]; if (name.equals("__SUB__")) { // __SUB__ returns the current subroutine being executed - RuntimeScalar selfReference = code.__SUB__; + RuntimeScalar selfReference = frame.selfReference; if (code.isQuotedRegexCallback) { // A qr// callback is compiled once but can be invoked by // different enclosing CVs. At execution time the active @@ -2217,7 +2237,7 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { } unwindEvalMethodInvocantHolds( evalMethodInvocantHoldDepthStack, - methodInvocantHolds); + methodInvocantHolds, evalBaseRegStack, registers); // Jump to eval catch handler pc = evalCatchStack.pop(); popEvalCallerFrame(frame); @@ -2401,7 +2421,7 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { } unwindEvalMethodInvocantHolds( evalMethodInvocantHoldDepthStack, - methodInvocantHolds); + methodInvocantHolds, evalBaseRegStack, registers); pc = evalCatchStack.pop(); popEvalCallerFrame(frame); RuntimeCode.decrementEvalDepth(); @@ -2902,7 +2922,8 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { // Push catch PC onto eval stack evalCatchStack.push(catchPc); frame.evalControlBlockDepthStack.push(new int[]{ - controlBlockStack.size(), labeledBlockStack.size()}); + controlBlockStack.size(), labeledBlockStack.size(), + visibleEvalFrame ? 1 : 0}); // Save first body register for scope cleanup on exception evalBaseRegStack.push(firstBodyReg); @@ -3183,7 +3204,7 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { Opcodes.STATE_RETRIEVE_ARRAY, Opcodes.STATE_IS_INITIALIZED, Opcodes.STATE_MARK_INITIALIZED, Opcodes.STATE_ALIAS_ARRAY, Opcodes.STATE_ALIAS_HASH, Opcodes.STATE_RETRIEVE_HASH -> { - pc = executeScopeOps(opcode, bytecode, pc, registers, code); + pc = executeScopeOps(opcode, bytecode, pc, registers, code, frame.selfReference); } // Group 6-8: System Calls and IPC (132-150) @@ -3224,7 +3245,9 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { case Opcodes.EVAL_STRING, Opcodes.SELECT_OP, Opcodes.LOAD_GLOB, Opcodes.LOAD_GLOB_CANONICAL, Opcodes.SLEEP_OP, Opcodes.ALARM_OP, Opcodes.DEREF_GLOB, Opcodes.DEREF_GLOB_NONSTRICT, Opcodes.LOAD_GLOB_DYNAMIC, Opcodes.DEREF_SCALAR_STRICT, - Opcodes.DEREF_SCALAR_NONSTRICT, Opcodes.CODE_DEREF_NONSTRICT, + Opcodes.DEREF_SCALAR_STRICT_TEMP, + Opcodes.DEREF_SCALAR_NONSTRICT, Opcodes.DEREF_SCALAR_NONSTRICT_TEMP, + Opcodes.CODE_DEREF_NONSTRICT, Opcodes.CODE_DEREF_STRICT, Opcodes.REFALIAS_CODE_REFERENCE, Opcodes.REFALIAS_SCALAR_REFERENCE, Opcodes.NAMED_CODE_REFERENCE, Opcodes.DIRECT_NAMED_CODE_CALL, @@ -3780,7 +3803,7 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { int catchPc = evalCatchStack.pop(); popEvalCallerFrame(frame); unwindEvalMethodInvocantHolds( - evalMethodInvocantHoldDepthStack, methodInvocantHolds); + evalMethodInvocantHoldDepthStack, methodInvocantHolds, evalBaseRegStack, registers); // Restore local variables pushed inside the eval block if (!evalLocalLevelStack.isEmpty()) { int relativeLevel = evalLocalLevelStack.pop(); @@ -3820,6 +3843,41 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { frame.propagatingException = e; throw e; } catch (Throwable e) { + if (e instanceof PerlNonLocalReturnException transfer + && transfer.targetCode == null && !evalCatchStack.isEmpty() + && !frame.evalControlBlockDepthStack.isEmpty() + && frame.evalControlBlockDepthStack.peek()[2] != 0) { + int catchPc = evalCatchStack.pop(); + int baseReg = evalBaseRegStack.peek(); + java.util.ArrayList locals = new java.util.ArrayList<>(); + BitSet myVars = code.myVarRegisters; + for (int i = myVars.nextSetBit(baseReg); + i >= 0 && i < registers.length; i = myVars.nextSetBit(i + 1)) { + if (registers[i] != null) locals.add(registers[i]); + } + int resultReg = bytecode[catchPc + 1]; + int resultContext = bytecode[catchPc + 2]; + if (resultContext == RuntimeContextType.RUNTIME) resultContext = callContext; + registers[resultReg] = RuntimeCode.finishEvalCallbackReturn( + transfer, locals.toArray(), resultContext, () -> { + popEvalCallerFrame(frame); + unwindEvalMethodInvocantHolds(evalMethodInvocantHoldDepthStack, + methodInvocantHolds, evalBaseRegStack, registers); + evalBaseRegStack.pop(); + if (!evalLocalLevelStack.isEmpty()) { + int relativeLevel = evalLocalLevelStack.pop(); + DynamicVariableManager.popToLocalLevel(savedLocalLevel + relativeLevel); + } + RuntimeCode.decrementEvalDepth(); + }); + for (int i = myVars.nextSetBit(baseReg); + i >= 0 && i < registers.length; i = myVars.nextSetBit(i + 1)) { + if (i != resultReg) registers[i] = null; + } + // Skip the failed-eval result opcode: this transfer succeeded. + pc = catchPc + 3; + continue outer; + } // Check if we're inside an eval block if (!evalCatchStack.isEmpty()) { Throwable evalException = e; @@ -3827,7 +3885,7 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { int catchPc = evalCatchStack.pop(); // Pop the catch handler popEvalCallerFrame(frame); unwindEvalMethodInvocantHolds( - evalMethodInvocantHoldDepthStack, methodInvocantHolds); + evalMethodInvocantHoldDepthStack, methodInvocantHolds, evalBaseRegStack, registers); // Scope exit cleanup for lexical variables allocated inside the eval body. // When die throws a PerlDieException, the SCOPE_EXIT_CLEANUP opcodes @@ -3936,11 +3994,23 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { // that were never stored via set() (refCount stayed at 0). // This handles eval STRING map/grep block closures that over-capture // all visible variables but are temporary and should release captures. - // Closures stored via set() have refCount > 0 and are skipped. + // Also retire unregistered temporary CODE scalar cells that only + // appear as borrowed values in an aliased @_ array. // This matches the JVM-compiled path where scopeExitCleanup releases // captures for CODE refs with refCount=0 (see RuntimeScalar.java // scopeExitCleanup special case for CODE refs). if (!frame.suspended && !createdClosures.isEmpty()) { + boolean releasedTemporaryCodeOwner = false; + for (RuntimeCode closure : createdClosures) { + if ((closure.capturedScalars == null && closure.capturedAggregates == null) + || RuntimeCode.isActiveCode(closure) + || (frame.returnedClosures != null + && frame.returnedClosures.contains(closure))) continue; + releasedTemporaryCodeOwner |= closure.releaseTemporaryCodeScalarOwners(); + } + if (releasedTemporaryCodeOwner) { + MortalList.flush(); + } for (RuntimeCode closure : createdClosures) { if ((closure.capturedScalars != null || closure.capturedAggregates != null) && closure.refCount == 0 @@ -4033,11 +4103,28 @@ private static void releaseMethodInvocantHoldsAbove( private static void unwindEvalMethodInvocantHolds( ArrayDeque evalMethodInvocantHoldDepthStack, - ArrayList methodInvocantHolds) { + ArrayList methodInvocantHolds, + ArrayDeque evalBaseRegStack, RuntimeBase[] registers) { + // Inline evals share their enclosing call frame. Retire only staging + // temporaries allocated in this eval; an outer call can still be + // evaluating its later arguments when the inner eval catches a die. + boolean releasedArguments = false; + if (!evalBaseRegStack.isEmpty()) { + for (int i = evalBaseRegStack.peek(); i < registers.length; i++) { + if (registers[i] instanceof RuntimeCode.CallArguments arguments) { + arguments.finish(); + registers[i] = null; + releasedArguments = true; + } + } + } if (!evalMethodInvocantHoldDepthStack.isEmpty()) { releaseMethodInvocantHoldsAbove( methodInvocantHolds, evalMethodInvocantHoldDepthStack.pop()); } + if (releasedArguments) { + MortalList.flushAboveMark(); + } } /** @@ -4519,7 +4606,8 @@ private static int executeArrayStringOps(int opcode, int[] bytecode, int pc, * Handles: RETRIEVE_BEGIN_*, LOCAL_SCALAR */ private static int executeScopeOps(int opcode, int[] bytecode, int pc, - RuntimeBase[] registers, InterpretedCode code) { + RuntimeBase[] registers, InterpretedCode code, + RuntimeScalar selfReference) { switch (opcode) { case Opcodes.RETRIEVE_BEGIN_SCALAR -> { return SlowOpcodeHandler.executeRetrieveBeginScalar(bytecode, pc, registers, code); @@ -4563,7 +4651,7 @@ private static int executeScopeOps(int opcode, int[] bytecode, int pc, // State belongs to the executing closure, not the interpreter // process. Falling back to an undefined code ref put every // anonymous-sub clone into the same global state cell. - RuntimeScalar codeRef = code.__SUB__ != null ? code.__SUB__ : new RuntimeScalar(); + RuntimeScalar codeRef = selfReference != null ? selfReference : new RuntimeScalar(); // Retrieve without removing (unlike RETRIEVE_BEGIN_SCALAR) RuntimeScalar stateVar = StateVariable.retrieveStateScalar(codeRef, varName, persistId); registers[rd] = stateVar; @@ -4580,7 +4668,7 @@ private static int executeScopeOps(int opcode, int[] bytecode, int pc, int nameIdx = bytecode[pc++]; int persistId = bytecode[pc++]; String varName = code.stringPool[nameIdx]; - RuntimeScalar codeRef = code.__SUB__ != null ? code.__SUB__ : new RuntimeScalar(); + RuntimeScalar codeRef = selfReference != null ? selfReference : new RuntimeScalar(); RuntimeArray stateArr = StateVariable.retrieveStateArray(codeRef, varName, persistId); registers[rd] = stateArr; RuntimeScalar isInit = StateVariable.isInitializedStateVariable(codeRef, varName, persistId); @@ -4594,7 +4682,7 @@ private static int executeScopeOps(int opcode, int[] bytecode, int pc, int rd = bytecode[pc++]; int nameIdx = bytecode[pc++]; int persistId = bytecode[pc++]; - RuntimeScalar codeRef = code.__SUB__ != null ? code.__SUB__ : new RuntimeScalar(); + RuntimeScalar codeRef = selfReference != null ? selfReference : new RuntimeScalar(); registers[rd] = StateVariable.retrieveStateArray(codeRef, code.stringPool[nameIdx], persistId); return pc; } @@ -4602,7 +4690,7 @@ private static int executeScopeOps(int opcode, int[] bytecode, int pc, int rd = bytecode[pc++]; int nameIdx = bytecode[pc++]; int persistId = bytecode[pc++]; - RuntimeScalar codeRef = code.__SUB__ != null ? code.__SUB__ : new RuntimeScalar(); + RuntimeScalar codeRef = selfReference != null ? selfReference : new RuntimeScalar(); registers[rd] = StateVariable.retrieveStateHash(codeRef, code.stringPool[nameIdx], persistId); return pc; } @@ -4611,7 +4699,7 @@ private static int executeScopeOps(int opcode, int[] bytecode, int pc, int sourceReg = bytecode[pc++]; int nameIdx = bytecode[pc++]; int persistId = bytecode[pc++]; - RuntimeScalar codeRef = code.__SUB__ != null ? code.__SUB__ : new RuntimeScalar(); + RuntimeScalar codeRef = selfReference != null ? selfReference : new RuntimeScalar(); RuntimeBase source = registers[sourceReg]; // Whole-array refaliasing may provide the aggregate directly // (for an array literal) or as the scalar array reference @@ -4628,7 +4716,7 @@ private static int executeScopeOps(int opcode, int[] bytecode, int pc, int sourceReg = bytecode[pc++]; int nameIdx = bytecode[pc++]; int persistId = bytecode[pc++]; - RuntimeScalar codeRef = code.__SUB__ != null ? code.__SUB__ : new RuntimeScalar(); + RuntimeScalar codeRef = selfReference != null ? selfReference : new RuntimeScalar(); RuntimeBase source = registers[sourceReg]; RuntimeHash hash = source instanceof RuntimeHash direct ? direct : source.scalar().hashDeref(); @@ -4642,7 +4730,7 @@ private static int executeScopeOps(int opcode, int[] bytecode, int pc, int nameIdx = bytecode[pc++]; int persistId = bytecode[pc++]; String varName = code.stringPool[nameIdx]; - RuntimeScalar codeRef = code.__SUB__ != null ? code.__SUB__ : new RuntimeScalar(); + RuntimeScalar codeRef = selfReference != null ? selfReference : new RuntimeScalar(); RuntimeHash stateHash = StateVariable.retrieveStateHash(codeRef, varName, persistId); registers[rd] = stateHash; RuntimeScalar isInit = StateVariable.isInitializedStateVariable(codeRef, varName, persistId); @@ -4657,7 +4745,7 @@ private static int executeScopeOps(int opcode, int[] bytecode, int pc, int nameIdx = bytecode[pc++]; int persistId = bytecode[pc++]; String varName = code.stringPool[nameIdx]; - RuntimeScalar codeRef = code.__SUB__ != null ? code.__SUB__ : new RuntimeScalar(); + RuntimeScalar codeRef = selfReference != null ? selfReference : new RuntimeScalar(); registers[rd] = StateVariable.retrieveStateScalar(codeRef, varName, persistId); return pc; } @@ -4666,7 +4754,7 @@ private static int executeScopeOps(int opcode, int[] bytecode, int pc, int nameIdx = bytecode[pc++]; int persistId = bytecode[pc++]; String varName = code.stringPool[nameIdx]; - RuntimeScalar codeRef = code.__SUB__ != null ? code.__SUB__ : new RuntimeScalar(); + RuntimeScalar codeRef = selfReference != null ? selfReference : new RuntimeScalar(); registers[rd] = StateVariable.isInitializedStateVariable(codeRef, varName, persistId); return pc; } @@ -4674,7 +4762,7 @@ private static int executeScopeOps(int opcode, int[] bytecode, int pc, int nameIdx = bytecode[pc++]; int persistId = bytecode[pc++]; String varName = code.stringPool[nameIdx]; - RuntimeScalar codeRef = code.__SUB__ != null ? code.__SUB__ : new RuntimeScalar(); + RuntimeScalar codeRef = selfReference != null ? selfReference : new RuntimeScalar(); StateVariable.markInitializedStateVariable(codeRef, varName, persistId); return pc; } @@ -4909,6 +4997,9 @@ private static int executeSpecialIO(int opcode, int[] bytecode, int pc, case Opcodes.DEREF_SCALAR_STRICT -> { return SlowOpcodeHandler.executeDerefScalarStrict(bytecode, pc, registers); } + case Opcodes.DEREF_SCALAR_STRICT_TEMP -> { + return SlowOpcodeHandler.executeDerefScalarStrictTemp(bytecode, pc, registers); + } case Opcodes.REJECT_LOCALIZE_REFERENCE -> { return SlowOpcodeHandler.executeRejectLocalizeReference(bytecode, pc, registers); } @@ -4930,6 +5021,9 @@ private static int executeSpecialIO(int opcode, int[] bytecode, int pc, case Opcodes.DEREF_SCALAR_NONSTRICT -> { return SlowOpcodeHandler.executeDerefScalarNonStrict(bytecode, pc, registers, code); } + case Opcodes.DEREF_SCALAR_NONSTRICT_TEMP -> { + return SlowOpcodeHandler.executeDerefScalarNonStrictTemp(bytecode, pc, registers, code); + } case Opcodes.CODE_DEREF_NONSTRICT -> { return SlowOpcodeHandler.executeCodeDerefNonStrict(bytecode, pc, registers, code); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index ce46f62092..f779d598b3 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -26,18 +26,18 @@ private static String posAggregateOperand(Node operand) { } private static void emitSubroutineExitCleanup(BytecodeCompiler bc, int returnReg) { - java.util.List scalarIdxs = bc.symbolTable.getMyScalarIndicesInScope(0); + java.util.List scalarIdxs = bc.myScalarCleanupIndicesInScope(0); for (int idx : scalarIdxs) { if (idx == returnReg) continue; bc.emit(Opcodes.SCOPE_EXIT_CLEANUP); bc.emitReg(idx); } - java.util.List hashIdxs = bc.symbolTable.getMyHashIndicesInScope(0); + java.util.List hashIdxs = bc.myHashCleanupIndicesInScope(0); for (int idx : hashIdxs) { bc.emit(Opcodes.SCOPE_EXIT_CLEANUP_HASH); bc.emitReg(idx); } - java.util.List arrayIdxs = bc.symbolTable.getMyArrayIndicesInScope(0); + java.util.List arrayIdxs = bc.myArrayCleanupIndicesInScope(0); for (int idx : arrayIdxs) { bc.emit(Opcodes.SCOPE_EXIT_CLEANUP_ARRAY); bc.emitReg(idx); @@ -1435,18 +1435,21 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode // Explicit 'return' bypasses the normal block-end cleanup. // Use return-specific cleanup opcodes so the returned register remains // readable by RETURN while lexical owner counts are still released. - java.util.List scalarIdxs = bytecodeCompiler.symbolTable.getMyScalarIndicesInScope(0); + exprReg = bytecodeCompiler.separateReturnRegister(exprReg); + bytecodeCompiler.emit(Opcodes.PREPARE_SCOPE_RETURN); + bytecodeCompiler.emitReg(exprReg); + java.util.List scalarIdxs = bytecodeCompiler.myScalarCleanupIndicesInScope(0); for (int idx : scalarIdxs) { bytecodeCompiler.emit(Opcodes.RETURN_SCOPE_CLEANUP); bytecodeCompiler.emitReg(idx); bytecodeCompiler.emitReg(exprReg); } - java.util.List hashIdxs = bytecodeCompiler.symbolTable.getMyHashIndicesInScope(0); + java.util.List hashIdxs = bytecodeCompiler.myHashCleanupIndicesInScope(0); for (int idx : hashIdxs) { bytecodeCompiler.emit(Opcodes.RETURN_SCOPE_CLEANUP_HASH); bytecodeCompiler.emitReg(idx); } - java.util.List arrayIdxs = bytecodeCompiler.symbolTable.getMyArrayIndicesInScope(0); + java.util.List arrayIdxs = bytecodeCompiler.myArrayCleanupIndicesInScope(0); for (int idx : arrayIdxs) { bytecodeCompiler.emit(Opcodes.RETURN_SCOPE_CLEANUP_ARRAY); bytecodeCompiler.emitReg(idx); diff --git a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java index 443ade7f68..ee5370002c 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java @@ -42,12 +42,30 @@ public static String disassemble(InterpretedCode interpretedCode) { int secReg = interpretedCode.bytecode[pc++]; sb.append("SCOPE_EXIT_CLEANUP r").append(secReg).append("\n"); break; + case Opcodes.DEREF_SCALAR_STRICT_TEMP: + int dstStrictTemp = interpretedCode.bytecode[pc++]; + int srcStrictTemp = interpretedCode.bytecode[pc++]; + sb.append("DEREF_SCALAR_STRICT_TEMP r").append(dstStrictTemp) + .append(" = ${r").append(srcStrictTemp).append("}\n"); + break; case Opcodes.UNBIND_ACTIVE_LEXICAL: int ualReg = interpretedCode.bytecode[pc++]; int ualName = interpretedCode.bytecode[pc++]; sb.append("UNBIND_ACTIVE_LEXICAL r").append(ualReg) .append(" ").append(interpretedCode.stringPool[ualName]).append("\n"); break; + case Opcodes.SNAPSHOT_SCOPE_RESULT: + int ssrDestination = interpretedCode.bytecode[pc++]; + int ssrSource = interpretedCode.bytecode[pc++]; + int ssrContext = interpretedCode.bytecode[pc++]; + sb.append("SNAPSHOT_SCOPE_RESULT r").append(ssrDestination) + .append(" source=r").append(ssrSource) + .append(" context=").append(ssrContext).append("\n"); + break; + case Opcodes.PREPARE_SCOPE_RETURN: + int psrReturnReg = interpretedCode.bytecode[pc++]; + sb.append("PREPARE_SCOPE_RETURN return=r").append(psrReturnReg).append("\n"); + break; case Opcodes.RETURN_SCOPE_CLEANUP: int rscReg = interpretedCode.bytecode[pc++]; int rscReturnReg = interpretedCode.bytecode[pc++]; @@ -1818,6 +1836,14 @@ public static String disassemble(InterpretedCode interpretedCode) { nameIdx = interpretedCode.bytecode[pc++]; sb.append("DEREF_SCALAR_NONSTRICT r").append(rd).append(" = ${r").append(rs).append("} pkg=").append(interpretedCode.stringPool[nameIdx]).append("\n"); break; + case Opcodes.DEREF_SCALAR_NONSTRICT_TEMP: + rd = interpretedCode.bytecode[pc++]; + rs = interpretedCode.bytecode[pc++]; + nameIdx = interpretedCode.bytecode[pc++]; + sb.append("DEREF_SCALAR_NONSTRICT_TEMP r").append(rd) + .append(" = ${r").append(rs).append("} pkg=") + .append(interpretedCode.stringPool[nameIdx]).append("\n"); + break; case Opcodes.CODE_DEREF_NONSTRICT: rd = interpretedCode.bytecode[pc++]; rs = interpretedCode.bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/FutureAsyncAwaitRuntime.java b/src/main/java/org/perlonjava/backend/bytecode/FutureAsyncAwaitRuntime.java index 4161e505cc..9f83c71722 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/FutureAsyncAwaitRuntime.java +++ b/src/main/java/org/perlonjava/backend/bytecode/FutureAsyncAwaitRuntime.java @@ -118,7 +118,6 @@ private static void attach(InterpreterSuspension suspension, AwaitState state) { state.callbackActive.set(false); } if (callbackWarningHandler != null) { - callbackWarningHandler.releaseClosureCapture(); RuntimeScalar.scopeExitCleanup(callbackWarningHandler); } } @@ -322,9 +321,7 @@ RuntimeScalar outerAfterResume() { return null; } if (outerWeak.value instanceof RuntimeBase referent - && !ReachabilityWalker.isReachableFromRoots(referent) - && !ReachabilityWalker.isReachableFromLiveScalarRegistry(referent) - && !ReachabilityWalker.isReachableFromLiveCodeCaptures(referent)) { + && !referent.hasPerlStrongOwner()) { WeakRefRegistry.clearWeakRefsTo(referent); return null; } @@ -446,7 +443,6 @@ private static RuntimeScalar retainCurrentWarningHandler() { // anonymous handler and its lexical captures before it emits the // self-abandonment warning. Keep the closure graph alive until the // queued resume has completed. - retained.retainClosureCapture(); return retained; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index fcb6814c5e..74d9a82737 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -474,6 +474,10 @@ public RuntimeList apply(RuntimeArray args, int callContext) { } MyVarCleanupStack.releaseOrTransferSocketOwnersOnReturn(cleanupMark, returned); return returned; + } catch (PerlNonLocalReturnException e) { + // The enclosing RuntimeCode boundary consumes the result; preserve + // its mortal owners while this frame's lexical cleanup is queued. + throw e; } catch (RuntimeException e) { if (!(e instanceof PerlExitException)) { MyVarCleanupStack.unwindTo(cleanupMark); @@ -547,6 +551,9 @@ public RuntimeList apply(String subroutineName, RuntimeArray args, int callConte } return RuntimeCode.coerceScalarCallResult( result, effectiveContext, callContext, !RuntimeCode.isLvalueCode(this)); + } catch (PerlNonLocalReturnException e) { + // A map/grep return carries a value through this frame's unwind. + throw e; } catch (RuntimeException e) { if (!(e instanceof PerlExitException)) { MyVarCleanupStack.unwindTo(cleanupMark); @@ -617,6 +624,7 @@ public InterpretedCode withCapturedVars(RuntimeBase[] capturedVars) { true, this.deparseSourceText ); + copy.installPadConstants(this.padConstants); copy.prototype = this.prototype; copy.isConstantCv = this.isConstantCv; copy.isLexicalConstantCv = this.isLexicalConstantCv; diff --git a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java index b8249d1ef9..9f860214cd 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java +++ b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java @@ -1099,6 +1099,7 @@ public static int executeCreateClosure(int[] bytecode, int pc, RuntimeBase[] reg // Without this, scopeExitCleanup() doesn't know the variable is still alive // via this closure, and may prematurely clear weak references to its value. java.util.List capturedScalars = new java.util.ArrayList<>(); + java.util.List capturedScalarOwnerSlots = new java.util.ArrayList<>(); java.util.List capturedAggregates = new java.util.ArrayList<>(); java.util.List capturedAggregateOwnerSlots = new java.util.ArrayList<>(); java.util.Set seenScalars = java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>()); @@ -1107,16 +1108,18 @@ public static int executeCreateClosure(int[] bytecode, int pc, RuntimeBase[] reg if (captured instanceof RuntimeScalar s) { if (!seenScalars.add(s)) continue; capturedScalars.add(s); - s.retainClosureCapture(); + capturedScalarOwnerSlots.add(s.retainClosureCaptureSlot(closureCode)); } else if (captured instanceof RuntimeArray || captured instanceof RuntimeHash) { if (!seenAggregates.add(captured)) continue; capturedAggregates.add(captured); - PerlOwnerSlot ownerSlot = captured.retainClosureCaptureOwner(); + PerlOwnerSlot ownerSlot = captured.retainClosureCaptureOwner(closureCode); capturedAggregateOwnerSlots.add(ownerSlot); } } if (!capturedScalars.isEmpty()) { closureCode.capturedScalars = capturedScalars.toArray(new RuntimeScalar[0]); + closureCode.capturedScalarOwnerSlots = + capturedScalarOwnerSlots.toArray(new PerlOwnerSlot[0]); } if (!capturedAggregates.isEmpty()) { closureCode.capturedAggregates = capturedAggregates.toArray(new RuntimeBase[0]); diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 2f18b0ca5d..9c0b8ab6e7 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -2787,6 +2787,17 @@ public class Opcodes { /** Restore a loop regex baseline and retain a fresh baseline for its next iteration. Format: register. */ public static final short RESTORE_LOOP_REGEX_STATE = 631; + /** Preserve rvalue reference owners before a nested scope exits. Format: rd sourceReg context. */ + public static final short SNAPSHOT_SCOPE_RESULT = 639; + + /** Dereference a compiler-owned scalar temporary and release it after lookup. Format: rd rs. */ + public static final short DEREF_SCALAR_STRICT_TEMP = 640; + /** Non-strict scalar dereference that consumes its scalar input. Format: rd rs pkgIdx. */ + public static final short DEREF_SCALAR_NONSTRICT_TEMP = 641; + + /** Acquire the subroutine return owner before any lexical cleanup. Format: returnReg. */ + public static final short PREPARE_SCOPE_RETURN = 642; + private Opcodes() { } // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index 4df3e1b7af..f828c8d55b 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -510,6 +510,32 @@ public static int executeDerefScalarStrict( return pc; } + public static int executeDerefScalarStrictTemp( + int[] bytecode, + int pc, + RuntimeBase[] registers) { + int rd = bytecode[pc++]; + int rs = bytecode[pc++]; + RuntimeBase temp = registers[rs]; + RuntimeScalar result = null; + boolean completed = false; + try { + result = guardReadOnlyScalarDeref( + registers[rs].scalar().scalarDeref()); + registers[rd] = result; + completed = true; + } finally { + try { + if (temp != result && temp instanceof RuntimeScalar scalar) { + RuntimeScalar.scopeExitCleanup(scalar); + } + } finally { + if (rd != rs || !completed) registers[rs] = null; + } + } + return pc; + } + public static int executeRejectLocalizeReference( int[] bytecode, int pc, RuntimeBase[] registers) { int rs = bytecode[pc++]; @@ -572,6 +598,35 @@ public static int executeDerefScalarNonStrict( return pc; } + public static int executeDerefScalarNonStrictTemp( + int[] bytecode, + int pc, + RuntimeBase[] registers, + InterpretedCode code) { + int rd = bytecode[pc++]; + int rs = bytecode[pc++]; + int pkgIdx = bytecode[pc++]; + String pkg = code.stringPool[pkgIdx]; + RuntimeBase temp = registers[rs]; + RuntimeScalar result = null; + boolean completed = false; + try { + result = guardReadOnlyScalarDeref( + registers[rs].scalar().scalarDerefNonStrict(pkg)); + registers[rd] = result; + completed = true; + } finally { + try { + if (temp != result && temp instanceof RuntimeScalar scalar) { + RuntimeScalar.scopeExitCleanup(scalar); + } + } finally { + if (rd != rs || !completed) registers[rs] = null; + } + } + return pc; + } + private static RuntimeScalar guardReadOnlyScalarDeref(RuntimeScalar target) { if (target instanceof RuntimeScalarReadOnly && !(target instanceof ReadOnlyAlias)) { diff --git a/src/main/java/org/perlonjava/backend/bytecode/SuspendedInterpreterFrame.java b/src/main/java/org/perlonjava/backend/bytecode/SuspendedInterpreterFrame.java index d9f60fa25b..3205d6cb5c 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SuspendedInterpreterFrame.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SuspendedInterpreterFrame.java @@ -3,6 +3,7 @@ import org.perlonjava.runtime.runtimetypes.RegexState; import org.perlonjava.runtime.runtimetypes.RuntimeBase; import org.perlonjava.runtime.runtimetypes.RuntimeCode; +import org.perlonjava.runtime.runtimetypes.RuntimeScalar; import org.perlonjava.runtime.runtimetypes.DynamicVariableManager; import java.util.ArrayDeque; @@ -23,6 +24,7 @@ public final class SuspendedInterpreterFrame { final RuntimeBase[] registers; final int callContext; final String subroutineName; + final RuntimeScalar selfReference; int pc; int virtualEvalFrameDepth; @@ -56,5 +58,8 @@ public final class SuspendedInterpreterFrame { this.registers = registers; this.callContext = callContext; this.subroutineName = subroutineName; + // ACTIVE_CODE owns this invocation. Borrow its CV value independently + // of a mutable glob slot, which Perl may undefine during the call. + this.selfReference = code.__SUB__ == null ? null : new RuntimeScalar(code.__SUB__); } } diff --git a/src/main/java/org/perlonjava/backend/jvm/Dereference.java b/src/main/java/org/perlonjava/backend/jvm/Dereference.java index 95dbea6751..dce58104f4 100644 --- a/src/main/java/org/perlonjava/backend/jvm/Dereference.java +++ b/src/main/java/org/perlonjava/backend/jvm/Dereference.java @@ -1089,6 +1089,13 @@ static void handleArrowOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod "(I)Lorg/perlonjava/runtime/runtimetypes/RuntimeCode$CallArguments;", false); mv.visitVarInsn(Opcodes.ASTORE, argsArraySlot); + // Argument evaluation can throw before the RuntimeCode call + // wrapper gets a chance to finish this staging frame. + Label argumentsStart = new Label(); + Label argumentsEnd = new Label(); + Label argumentsFailure = new Label(); + Label argumentsReady = new Label(); + mv.visitLabel(argumentsStart); for (int index = 0; index < argCount; index++) { int argSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); boolean pooledArg = argSlot >= 0; @@ -1102,6 +1109,20 @@ static void handleArrowOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod "(Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;)V", false); if (pooledArg) emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); } + mv.visitLabel(argumentsEnd); + mv.visitJumpInsn(Opcodes.GOTO, argumentsReady); + mv.visitLabel(argumentsFailure); + // Keep the original throwable on the operand stack while + // releasing only this call's acquired argument owners. + mv.visitVarInsn(Opcodes.ALOAD, argsArraySlot); + mv.visitInsn(Opcodes.SWAP); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "org/perlonjava/runtime/runtimetypes/RuntimeCode$CallArguments", "abandon", + "(Ljava/lang/Throwable;)Ljava/lang/Throwable;", false); + mv.visitInsn(Opcodes.ATHROW); + mv.visitLabel(argumentsReady); + // Register after the body so inner eval handlers take priority. + mv.visitTryCatchBlock(argumentsStart, argumentsEnd, argumentsFailure, null); } // The call context is NOT stored in a local slot (see EmitSubroutine.java comment for why: diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java index b2739ca0bc..e4dd971750 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java @@ -521,6 +521,12 @@ static void handleReturnOperator(EmitterVisitor emitterVisitor, OperatorNode nod false); } + ctx.mv.visitVarInsn(Opcodes.ILOAD, 2); + ctx.mv.visitInsn(ctx.javaClassInfo.isLvalueSubroutine ? Opcodes.ICONST_0 : Opcodes.ICONST_1); + ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeCode", "prepareScopeReturn", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;IZ)Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;", + false); ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/RuntimeCode", "materializeReturnedIoAliases", @@ -593,6 +599,17 @@ static void handleReturnOperator(EmitterVisitor emitterVisitor, OperatorNode nod false); } for (int idx : arrayIndices) { + ctx.javaClassInfo.loadSpillRef(ctx.mv, spillRef); + ctx.mv.visitVarInsn(Opcodes.ALOAD, idx); + ctx.mv.visitVarInsn(Opcodes.ILOAD, 2); + ctx.mv.visitInsn(ctx.javaClassInfo.isLvalueSubroutine + ? Opcodes.ICONST_0 : Opcodes.ICONST_1); + ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeCode", + "prepareLocalArrayReturn", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;Lorg/perlonjava/runtime/runtimetypes/RuntimeArray;IZ)Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;", + false); + ctx.javaClassInfo.storeSpillRef(ctx.mv, spillRef); ctx.mv.visitVarInsn(Opcodes.ALOAD, idx); ctx.javaClassInfo.loadSpillRef(ctx.mv, spillRef); ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitLiteral.java b/src/main/java/org/perlonjava/backend/jvm/EmitLiteral.java index 5169283b71..b4cafe56f8 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitLiteral.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitLiteral.java @@ -359,16 +359,10 @@ private static void emitCachedStringLiteral( } /** - * Emits a string operand for {@code \\} (ref-to-literal) using the string cache's - * singleton scalars ({@link RuntimeScalarCache#getScalarByteString(int)} / - * {@link RuntimeScalarCache#getScalarString(int)}) for cacheable literals, and records - * them on {@link JavaClassInfo#addPadConstant}. - * - *

Ordinary {@link #emitString} uses {@link RuntimeScalarCache#materializeByteStringLiteral(int)} - * / {@link RuntimeScalarCache#materializeStringLiteral(int)} so each literal occurrence - * has a distinct SV (needed for {@code pos()} / {@code \\G}). Ref-to-literal in Perl shares one - * const SV in the CV; weak refs (e.g. Moo {@code weak_ref}) and {@link RuntimeCode#clearPadConstantWeakRefs} - * must target that same object identity.

+ * Emits the stable scalar owned by this optree for a backslash-addressed + * string literal. Each source occurrence gets its own read-only scalar; + * the owning CV and any enclosing anonymous-code template retain it by + * explicit pad-constant slots. */ public static void emitStringForRefToLiteral(EmitterContext ctx, StringNode node) { if (ctx.contextType == RuntimeContextType.VOID) { @@ -379,54 +373,56 @@ public static void emitStringForRefToLiteral(EmitterContext ctx, StringNode node emitStringValue(mv, node.value); return; } - if (node.isVString) { - // No cache slot; same as plain literal (optree reaping may not match Perl for \\v). + if (ctx.javaClassInfo == null) { + // Some deferred compilation contexts do not have an owning class yet. emitString(ctx, node); return; } - if (!node.forceUnicodeString && (node.forceByteString - || (!ctx.symbolTable.isStrictOptionEnabled(HINT_UTF8) - && !ctx.compilerOptions.isUnicodeSource))) { - boolean hasWideChars = false; + // RuntimeScalarCache is only an interning aid for ordinary literals. A + // backslash-addressed literal is a distinct Perl scalar identity even + // when it is too large for that cache or represents a v-string. Build + // that identity once while compiling and let the generated CODE retain + // it through its explicit pad-constant owner slot. + RuntimeScalarReadOnly constant = new RuntimeScalarReadOnly(node.value); + if (node.isVString) { + constant.type = RuntimeScalarType.VSTRING; + } else if (isByteStringLiteral(ctx, node)) { + constant.type = RuntimeScalarType.BYTE_STRING; + } + int padIndex = ctx.javaClassInfo.addPadConstant(constant); + emitPadConstantReference(ctx, padIndex); + } + + private static boolean isByteStringLiteral(EmitterContext ctx, StringNode node) { + if (node.forceUnicodeString) return false; + if (!node.forceByteString) { + boolean asciiOnly = true; for (int i = 0; i < node.value.length(); i++) { - if (node.value.charAt(i) > 255) { - hasWideChars = true; + if (node.value.charAt(i) > 127) { + asciiOnly = false; break; } } - if (!hasWideChars) { - int stringIndex = RuntimeScalarCache.getOrCreateByteStringIndex(node.value); - if (stringIndex >= 0) { - mv.visitLdcInsn(stringIndex); - mv.visitMethodInsn( - Opcodes.INVOKESTATIC, - "org/perlonjava/runtime/runtimetypes/RuntimeScalarCache", - "getScalarByteString", - "(I)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", - false); - ctx.javaClassInfo.addPadConstant(RuntimeScalarCache.getScalarByteString(stringIndex)); - return; - } - emitString(ctx, node); - return; + if (!asciiOnly && ctx.symbolTable.isStrictOptionEnabled(HINT_UTF8)) { + return false; } - // fall through — wide chars use UTF-8 string path (same as emitString) } - - int stringIndex = RuntimeScalarCache.getOrCreateStringIndex(node.value); - if (stringIndex >= 0) { - mv.visitLdcInsn(stringIndex); - mv.visitMethodInsn( - Opcodes.INVOKESTATIC, - "org/perlonjava/runtime/runtimetypes/RuntimeScalarCache", - "getScalarString", - "(I)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", - false); - ctx.javaClassInfo.addPadConstant(RuntimeScalarCache.getScalarString(stringIndex)); - } else { - emitString(ctx, node); + for (int i = 0; i < node.value.length(); i++) { + if (node.value.charAt(i) > 255) return false; } + return true; + } + + private static void emitPadConstantReference(EmitterContext ctx, int padIndex) { + MethodVisitor mv = ctx.mv; + mv.visitLdcInsn(ctx.javaClassInfo.javaClassName); + mv.visitLdcInsn(padIndex); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeCode", + "getPadConstant", + "(Ljava/lang/String;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", + false); } /** diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java index f03a09b61f..2ebc23218d 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java @@ -2130,7 +2130,7 @@ static void handleCreateReference(EmitterVisitor emitterVisitor, OperatorNode no } if (node.operand instanceof StringNode strNode) { - // Singleton cache scalars + padConstants; see EmitLiteral.emitStringForRefToLiteral. + // Per-optree scalar identities + pad-owner slots; see EmitLiteral.emitStringForRefToLiteral. EmitLiteral.emitStringForRefToLiteral( emitterVisitor.with(contextType).ctx, strNode); } else if (node.operand instanceof NumberNode numberNode) { diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitStatement.java b/src/main/java/org/perlonjava/backend/jvm/EmitStatement.java index 16da6a0de9..4fc0c8ae06 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitStatement.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitStatement.java @@ -168,6 +168,17 @@ private static void emitScopeExitNullStores( // scope exits (e.g., Foo->new()->method() chain temporaries) may need processing. boolean needsCleanup = !scalarIndices.isEmpty() || !hashIndices.isEmpty() || !arrayIndices.isEmpty(); + if (needsCleanup && returnedLvalueSlot >= 0) { + ctx.mv.visitVarInsn(Opcodes.ALOAD, returnedLvalueSlot); + ctx.mv.visitVarInsn(Opcodes.ILOAD, 2); + ctx.mv.visitInsn(ctx.javaClassInfo.isLvalueSubroutine ? Opcodes.ICONST_0 : Opcodes.ICONST_1); + ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeCode", "prepareScopeReturn", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;IZ)Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;", + false); + ctx.mv.visitVarInsn(Opcodes.ASTORE, returnedLvalueSlot); + } + // Phase 1: Run scopeExitCleanup for scalar variables. // This defers refCount decrements for blessed references with DESTROY, // and handles IO fd recycling for anonymous filehandle globs. @@ -200,8 +211,19 @@ private static void emitScopeExitNullStores( false); } for (int idx : arrayIndices) { - ctx.mv.visitVarInsn(Opcodes.ALOAD, idx); if (returnedLvalueSlot >= 0) { + ctx.mv.visitVarInsn(Opcodes.ALOAD, returnedLvalueSlot); + ctx.mv.visitVarInsn(Opcodes.ALOAD, idx); + ctx.mv.visitVarInsn(Opcodes.ILOAD, 2); + ctx.mv.visitInsn(ctx.javaClassInfo.isLvalueSubroutine + ? Opcodes.ICONST_0 : Opcodes.ICONST_1); + ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeCode", + "prepareLocalArrayReturn", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;Lorg/perlonjava/runtime/runtimetypes/RuntimeArray;IZ)Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;", + false); + ctx.mv.visitVarInsn(Opcodes.ASTORE, returnedLvalueSlot); + ctx.mv.visitVarInsn(Opcodes.ALOAD, idx); ctx.mv.visitVarInsn(Opcodes.ALOAD, returnedLvalueSlot); ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/MortalList", @@ -210,6 +232,7 @@ private static void emitScopeExitNullStores( false); continue; } + ctx.mv.visitVarInsn(Opcodes.ALOAD, idx); ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/MortalList", "scopeExitCleanupArray", @@ -436,8 +459,7 @@ public static void emitIf(EmitterVisitor emitterVisitor, IfNode node) { int scopeIndex = emitterVisitor.ctx.symbolTable.enterScope(); node.thenBranch.accept(branchVisitor(emitterVisitor, node.thenBranch)); - emitScopeExitNullStores(emitterVisitor.ctx, scopeIndex, - emitterVisitor.ctx.contextType == RuntimeContextType.VOID); + emitConditionalScopeExit(emitterVisitor.ctx, scopeIndex); emitterVisitor.ctx.symbolTable.exitScope(scopeIndex); for (int i = 0; i < branchLabelsPushed; i++) { @@ -452,8 +474,7 @@ public static void emitIf(EmitterVisitor emitterVisitor, IfNode node) { int scopeIndex = emitterVisitor.ctx.symbolTable.enterScope(); node.elseBranch.accept(branchVisitor(emitterVisitor, node.elseBranch)); - emitScopeExitNullStores(emitterVisitor.ctx, scopeIndex, - emitterVisitor.ctx.contextType == RuntimeContextType.VOID); + emitConditionalScopeExit(emitterVisitor.ctx, scopeIndex); emitterVisitor.ctx.symbolTable.exitScope(scopeIndex); for (int i = 0; i < branchLabelsPushed; i++) { @@ -559,8 +580,7 @@ public static void emitIf(EmitterVisitor emitterVisitor, IfNode node) { emitterVisitor.ctx.mv.visitLabel(endLabel); // Exit the scope in the symbol table - emitScopeExitNullStores(emitterVisitor.ctx, scopeIndex, - emitterVisitor.ctx.contextType == RuntimeContextType.VOID); + emitConditionalScopeExit(emitterVisitor.ctx, scopeIndex); emitterVisitor.ctx.symbolTable.exitScope(scopeIndex); for (int i = 0; i < branchLabelsPushed; i++) { @@ -570,6 +590,31 @@ public static void emitIf(EmitterVisitor emitterVisitor, IfNode node) { if (CompilerOptions.DEBUG_ENABLED) emitterVisitor.ctx.logDebug("IF end"); } + /** The conditional's result must own returned references before its lexicals end. */ + private static void emitConditionalScopeExit(EmitterContext ctx, int scopeIndex) { + if (ctx.contextType != RuntimeContextType.VOID + && !ctx.symbolTable.getMyVariableIndicesInScope(scopeIndex).isEmpty()) { + if (ctx.contextType == RuntimeContextType.RUNTIME) { + ctx.mv.visitVarInsn(Opcodes.ILOAD, 2); + } else { + ctx.mv.visitLdcInsn(ctx.contextType); + } + ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeCode", "prepareScopeResult", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;", + false); + // Native magic cleanup can flush its own releases. Keep result + // owners below this mark until the caller receives the expression. + ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/MortalList", "pushMark", "()V", false); + emitScopeExitNullStores(ctx, scopeIndex, false); + ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/MortalList", "popAndFlush", "()V", false); + return; + } + emitScopeExitNullStores(ctx, scopeIndex, ctx.contextType == RuntimeContextType.VOID); + } + /** Return the branch that emitIf will retain, or null when both branches remain dynamic. */ static Boolean constantIfConditionValue(IfNode node, String currentPackage) { Boolean constantValue = ConstantFoldingVisitor.getConstantConditionValue( diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java index a4ba378758..7c3486c917 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java @@ -432,11 +432,31 @@ && isDirectLeafIntegerAddition(node.block, directLeafCaptures, deparseFlags |= RuntimeCode.DEPARSE_FLAG_WARNINGS; } - // Transfer pad constants (cached string literals referenced via \) from compile time - // to a registry so makeCodeObject() can attach them to the RuntimeCode at runtime. - if (subCtx.javaClassInfo.padConstants != null && !subCtx.javaClassInfo.padConstants.isEmpty()) { + // Register this optree's constants and nested anonymous optrees. + // The parent CV owns nested anonymous constants as the template + // from which later closure instances are created. + RuntimeBase[] padConstants = subCtx.javaClassInfo.padConstants == null + ? new RuntimeBase[0] + : subCtx.javaClassInfo.padConstants.toArray(new RuntimeBase[0]); + Map embeddedPadConstants = + subCtx.javaClassInfo.embeddedPadConstants; + Map> embeddedPadDescendants = + subCtx.javaClassInfo.embeddedPadConstantDescendants; + if (padConstants.length != 0 || !embeddedPadConstants.isEmpty()) { RuntimeCode.registerPadConstants(subCtx.javaClassInfo.javaClassName, - subCtx.javaClassInfo.padConstants.toArray(new RuntimeBase[0])); + padConstants, embeddedPadConstants, embeddedPadDescendants); + boolean embeddedAnonymousOptree = !node.useTryCatch + && (node.name == null || node.name.equals("") + || node.getBooleanAnnotation("isMapGrepBlock") + || node.getBooleanAnnotation("inheritsSelfReference") + || node.getBooleanAnnotation("regexCallbackPseudoBlock")); + if (embeddedAnonymousOptree && ctx.javaClassInfo != null) { + ctx.javaClassInfo.addEmbeddedPadConstants( + subCtx.javaClassInfo.javaClassName, + padConstants, + embeddedPadConstants, + embeddedPadDescendants); + } } // Direct instantiation approach - no reflection needed! diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java index 01b1ab6d9a..6e43d78a99 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java @@ -3273,6 +3273,19 @@ static void handleMyOperator(EmitterVisitor emitterVisitor, OperatorNode node) { false); } + if (operator.equals("my") && (sigil.equals("@") || sigil.equals("%"))) { + // The active pad map is a strong runtime owner of this + // aggregate. Give it an identity after the lexical has + // been registered so scope cleanup can retire it. + emitterVisitor.ctx.mv.visitLdcInsn(var); + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, varIndex); + emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeCode", + "registerCurrentActiveLexical", + "(Ljava/lang/String;Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;)V", + false); + } + // Emit runtime attribute dispatch for my/state variables. // For 'our', attributes were already dispatched at compile time. if (!operator.equals("our") && node.annotations != null diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java index f59b675033..d7e74f2126 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -1114,7 +1114,39 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean "org/perlonjava/runtime/runtimetypes/PerlNonLocalReturnException"); Label notNonLocalReturn = new Label(); mv.visitJumpInsn(Opcodes.IFEQ, notNonLocalReturn); - mv.visitInsn(Opcodes.ATHROW); // Re-throw PerlNonLocalReturnException + // Untargeted map/grep return belongs to this eval boundary. + // A targeted try/finally return continues to its containing CV. + mv.visitInsn(Opcodes.DUP); + mv.visitTypeInsn(Opcodes.CHECKCAST, + "org/perlonjava/runtime/runtimetypes/PerlNonLocalReturnException"); + mv.visitFieldInsn(Opcodes.GETFIELD, + "org/perlonjava/runtime/runtimetypes/PerlNonLocalReturnException", + "targetCode", "Lorg/perlonjava/runtime/runtimetypes/RuntimeCode;"); + Label targetedReturn = new Label(); + mv.visitJumpInsn(Opcodes.IFNONNULL, targetedReturn); + mv.visitTypeInsn(Opcodes.CHECKCAST, + "org/perlonjava/runtime/runtimetypes/PerlNonLocalReturnException"); + java.util.List returnCleanupLocals = evalCleanupLocals == null + ? java.util.List.of() : new java.util.ArrayList<>( + new java.util.LinkedHashSet<>(evalCleanupLocals)); + mv.visitLdcInsn(returnCleanupLocals.size()); + mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object"); + for (int i = 0; i < returnCleanupLocals.size(); i++) { + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(i); + mv.visitVarInsn(Opcodes.ALOAD, returnCleanupLocals.get(i)); + mv.visitInsn(Opcodes.AASTORE); + } + mv.visitVarInsn(Opcodes.ILOAD, 2); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeCode", + "finishEvalCallbackReturn", + "(Lorg/perlonjava/runtime/runtimetypes/PerlNonLocalReturnException;[Ljava/lang/Object;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeList;", false); + mv.visitVarInsn(Opcodes.ASTORE, returnListSlot); + emitEvalDepthDecrement(mv); + mv.visitJumpInsn(Opcodes.GOTO, endCatch); + mv.visitLabel(targetedReturn); + mv.visitInsn(Opcodes.ATHROW); mv.visitLabel(notNonLocalReturn); // Track eval depth for $^S: RuntimeCode.evalDepth-- @@ -2081,6 +2113,16 @@ public static RuntimeCode createRuntimeCode( System.err.println("Note: JVM compilation succeeded."); } RuntimeCode code = wrapAsCompiledCode(generatedClass, ctx, ast); + RuntimeBase[] padConstants = ctx.javaClassInfo.padConstants == null + ? new RuntimeBase[0] + : ctx.javaClassInfo.padConstants.toArray(new RuntimeBase[0]); + if (padConstants.length != 0 || !ctx.javaClassInfo.embeddedPadConstants.isEmpty()) { + code.installPadConstantTree( + ctx.javaClassInfo.javaClassName, + padConstants, + ctx.javaClassInfo.embeddedPadConstants, + ctx.javaClassInfo.embeddedPadConstantDescendants); + } code.setJvmClosureFrameRequired(ctx.javaClassInfo.jvmClosureFrameRequired); code.setPristineArgsSnapshotRequired( ctx.javaClassInfo.pristineArgsSnapshotRequired); diff --git a/src/main/java/org/perlonjava/backend/jvm/JavaClassInfo.java b/src/main/java/org/perlonjava/backend/jvm/JavaClassInfo.java index 81789dbc7d..91294323e1 100644 --- a/src/main/java/org/perlonjava/backend/jvm/JavaClassInfo.java +++ b/src/main/java/org/perlonjava/backend/jvm/JavaClassInfo.java @@ -294,6 +294,10 @@ public boolean isCapturedVariableIndex(int index) { * are cleared to emulate Perl 5's "optree reaping" behavior. */ public List padConstants; + /** Constants owned by anonymous optrees embedded in this code body. */ + public final Map embeddedPadConstants = new LinkedHashMap<>(); + /** Embedded optree descendant class names, grouped by their immediate template. */ + public final Map> embeddedPadConstantDescendants = new LinkedHashMap<>(); /** * Constructs a new JavaClassInfo object. @@ -324,11 +328,38 @@ public JavaClassInfo() { /** * Records a cached constant that was referenced via backslash in this subroutine. */ - public void addPadConstant(RuntimeBase constant) { + public int addPadConstant(RuntimeBase constant) { if (padConstants == null) { padConstants = new ArrayList<>(); } padConstants.add(constant); + return padConstants.size() - 1; + } + + /** Retain an embedded anonymous optree's constants with this CV template. */ + public void addEmbeddedPadConstants( + String className, + RuntimeBase[] constants, + Map descendants, + Map> descendantTrees) { + if (className != null && !className.isEmpty()) { + embeddedPadConstants.put(className, + constants == null ? new RuntimeBase[0] : constants.clone()); + Set descendantsForClass = descendants == null + ? new LinkedHashSet<>() : new LinkedHashSet<>(descendants.keySet()); + embeddedPadConstantDescendants.put(className, descendantsForClass); + } + if (descendants != null) { + descendants.forEach((nestedClassName, nestedConstants) -> + embeddedPadConstants.put(nestedClassName, + nestedConstants == null ? new RuntimeBase[0] : nestedConstants.clone())); + } + if (descendantTrees != null) { + descendantTrees.forEach((nestedClassName, nestedClasses) -> + embeddedPadConstantDescendants.put(nestedClassName, + nestedClasses == null ? new LinkedHashSet<>() + : new LinkedHashSet<>(nestedClasses))); + } } public int acquireSpillSlot() { diff --git a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java index cc85db15a5..c5f79d3941 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java @@ -2524,6 +2524,7 @@ && isLexicalSubStorageReferenced(explicitlyUsedVars, entry.name()) installClosureCaptureMetadata(placeholder, paramList); placeholder.cvStartFile = compiledCode.cvStartFile; placeholder.cvStartLine = compiledCode.cvStartLine; + compiledCode.transferPadConstantTreeTo(placeholder); } else if (runtimeCode instanceof InterpretedCode interpretedCode) { // InterpretedCode path - update placeholder in-place (not replace codeRef.value) @@ -2577,6 +2578,10 @@ && isLexicalSubStorageReferenced(explicitlyUsedVars, entry.name()) installClosureCaptureMetadata(placeholder, materializedCaptures); placeholder.cvStartFile = interpretedCode.cvStartFile; placeholder.cvStartLine = interpretedCode.cvStartLine; + // The published placeholder is the Perl-owned CV. Move + // the interpreter optree's constant edges onto it so the + // temporary executable wrapper cannot pin pad literals. + interpretedCode.transferPadConstantTreeTo(placeholder); } } catch (VerifyError ve) { // VerifyError extends Error (not Exception), so it's not caught by catch(Exception). @@ -2630,19 +2635,12 @@ && isLexicalSubStorageReferenced(explicitlyUsedVars, entry.name()) installClosureCaptureMetadata(placeholder, materializedCaptures); placeholder.cvStartFile = interpretedCode.cvStartFile; placeholder.cvStartLine = interpretedCode.cvStartLine; + interpretedCode.transferPadConstantTreeTo(placeholder); } catch (Exception e) { // Handle any exceptions during subroutine creation throw new PerlCompilerException("Subroutine error: " + e.getMessage()); } - // Transfer pad constants (cached string literals referenced via \) - // from compile context to the RuntimeCode for optree reaping. - if (newCtx.javaClassInfo.padConstants != null && !newCtx.javaClassInfo.padConstants.isEmpty()) { - placeholder.padConstants = newCtx.javaClassInfo.padConstants.toArray( - new org.perlonjava.runtime.runtimetypes.RuntimeBase[0]); - GlobalVariable.invalidateGlobalCodeRefGraphRoots(); - } - // Clear the compilerSupplier once done (use the captured placeholder variable) // This prevents the Supplier from being invoked multiple times placeholder.compilerSupplier = null; @@ -2748,6 +2746,7 @@ private static void installClosureCaptureMetadata( } ArrayList capturedScalars = new ArrayList<>(); + ArrayList capturedScalarOwnerSlots = new ArrayList<>(); ArrayList capturedAggregates = new ArrayList<>(); ArrayList capturedAggregateOwnerSlots = new ArrayList<>(); for (int i = 0; i < capturedValues.size(); i++) { @@ -2761,15 +2760,17 @@ private static void installClosureCaptureMetadata( } if (value instanceof RuntimeScalar scalar) { capturedScalars.add(scalar); - scalar.retainClosureCapture(); + capturedScalarOwnerSlots.add(scalar.retainClosureCaptureSlot(code)); } else if (value instanceof RuntimeArray || value instanceof RuntimeHash) { RuntimeBase aggregate = (RuntimeBase) value; capturedAggregates.add(aggregate); - capturedAggregateOwnerSlots.add(aggregate.retainClosureCaptureOwner()); + capturedAggregateOwnerSlots.add(aggregate.retainClosureCaptureOwner(code)); } } if (!capturedScalars.isEmpty()) { code.capturedScalars = capturedScalars.toArray(new RuntimeScalar[0]); + code.capturedScalarOwnerSlots = + capturedScalarOwnerSlots.toArray(new PerlOwnerSlot[0]); } if (!capturedAggregates.isEmpty()) { code.capturedAggregates = capturedAggregates.toArray(new RuntimeBase[0]); @@ -2777,7 +2778,8 @@ private static void installClosureCaptureMetadata( capturedAggregateOwnerSlots.toArray(new PerlOwnerSlot[0]); } GlobalVariable.invalidateGlobalCodeRefGraphRoots(); - if (!capturedScalars.isEmpty() || !capturedAggregates.isEmpty()) { + if ((!capturedScalars.isEmpty() || !capturedAggregates.isEmpty()) + && code.refCount == -1) { code.refCount = 0; } } diff --git a/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java b/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java index 6c9cf65989..c647722688 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java @@ -225,6 +225,7 @@ private static RuntimeScalar bless(RuntimeScalar runtimeScalar, RuntimeScalar cl referent.refCount = 1; referent.recordOwner(runtimeScalar, "rebless from untracked"); runtimeScalar.refCountOwned = true; + referent.recordActiveOwner(runtimeScalar); } else { // First bless: start at refCount=1 and add to MortalList. // The mortal entry will decrement back to 0 at the next @@ -258,6 +259,7 @@ private static RuntimeScalar bless(RuntimeScalar runtimeScalar, RuntimeScalar cl if (existingScalarOwner) { referent.recordOwner(runtimeScalar, "first bless of existing scalar ref"); runtimeScalar.refCountOwned = true; + referent.recordActiveOwner(runtimeScalar); } referent.acquireTransientTraceOwner("bless mortal temporary", "ReferenceOperators.first bless"); diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/HTMLParser.java b/src/main/java/org/perlonjava/runtime/perlmodule/HTMLParser.java index a3c366e79c..ff85335799 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/HTMLParser.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/HTMLParser.java @@ -133,7 +133,7 @@ public static RuntimeList _alloc_pstate(RuntimeArray args, int ctx) { handlers.put(event + "_cb", scalarUndef); handlers.put(event + "_argspec", scalarUndef); } - pstate.put("_handlers", handlers.createReference()); + pstate.put("_handlers", handlers.createAnonymousReference()); // State tracking pstate.put("_parsing", scalarFalse); @@ -147,7 +147,7 @@ public static RuntimeList _alloc_pstate(RuntimeArray args, int ctx) { pstate.put("_input_is_byte_string", scalarFalse); pstate.put("_literal_mode", new RuntimeScalar("")); pstate.put("_ignore_current_element", scalarFalse); - pstate.put("_ignore_elements", new RuntimeArray().createReference()); + pstate.put("_ignore_elements", new RuntimeArray().createAnonymousReference()); pstate.put("_pending_end_tag", new RuntimeScalar("")); pstate.put("_bool_attr_val", scalarUndef); pstate.put("_in_cdata", scalarFalse); @@ -162,7 +162,7 @@ public static RuntimeList _alloc_pstate(RuntimeArray args, int ctx) { pstate.put("_event_column", new RuntimeScalar(1)); // Store in self - selfHash.put("_hparser_xs_state", pstate.createReference()); + selfHash.put("_hparser_xs_state", pstate.createAnonymousReference()); return new RuntimeList(); } @@ -369,7 +369,7 @@ public static RuntimeList tagListAccessor(RuntimeArray args, int ctx) { RuntimeArray.push(replacement, arg); } } - pstate.put("_ignore_elements", replacement.createReference()); + pstate.put("_ignore_elements", replacement.createAnonymousReference()); } RuntimeList result = new RuntimeList(); for (int i = 0; i < previous.size(); i++) result.add(previous.get(i)); @@ -558,48 +558,44 @@ private static void fireEvent(RuntimeScalar self, RuntimeHash selfHash, RuntimeH // Build event data per argspec and push as array ref onto accumulator RuntimeArray accum = (RuntimeArray) cb.value; RuntimeArray eventData = buildEventDataFromArgspec(argspec, eventName, eventArgs, self, false, pstate); - RuntimeArray.push(accum, eventData.createReference()); - } else if (cb.type == RuntimeScalarType.STRING || cb.type == RuntimeScalarType.BYTE_STRING) { - // Method name - call as $self->method(...) - String methodName = cb.toString(); + RuntimeArray.push(accum, eventData.createAnonymousReference()); + } else if (cb.type == RuntimeScalarType.STRING || cb.type == RuntimeScalarType.BYTE_STRING + || cb.type == RuntimeScalarType.REFERENCE || cb.type == RuntimeScalarType.CODE) { + boolean methodCallback = cb.type == RuntimeScalarType.STRING + || cb.type == RuntimeScalarType.BYTE_STRING; RuntimeArray callArgs = new RuntimeArray(); - RuntimeArray.push(callArgs, self); - // Build args from argspec if available, otherwise pass raw eventArgs - // skipSelf=true: "self" in argspec specifies the invocant for method dispatch - // but should NOT be duplicated in the method arguments - if (!argspec.isEmpty()) { - RuntimeArray eventData = buildEventDataFromArgspec(argspec, eventName, eventArgs, self, true, pstate); - for (int idx = 0; idx < eventData.size(); idx++) { - RuntimeArray.push(callArgs, eventData.get(idx)); - } - } else { - for (RuntimeScalar arg : eventArgs) { - RuntimeArray.push(callArgs, arg); - } - } - // Look up method in the object's class hierarchy using the blessed class - int blessId = RuntimeScalarType.blessedId(self); - String className = (blessId != 0) ? - NameNormalizer.getBlessStr(blessId) : "HTML::Parser"; - RuntimeScalar method = InheritanceResolver.findMethodInHierarchy( - methodName, className, null, 0); - if (method != null) { - RuntimeCode.apply(method, callArgs, RuntimeContextType.VOID); - } - } else if (cb.type == RuntimeScalarType.REFERENCE || cb.type == RuntimeScalarType.CODE) { - // Code reference - call directly - RuntimeArray callArgs = new RuntimeArray(); - if (!argspec.isEmpty()) { - RuntimeArray eventData = buildEventDataFromArgspec(argspec, eventName, eventArgs, self, false, pstate); - for (int idx = 0; idx < eventData.size(); idx++) { - RuntimeArray.push(callArgs, eventData.get(idx)); + RuntimeArray eventData = null; + try { + if (methodCallback) RuntimeArray.push(callArgs, self); + if (!argspec.isEmpty()) { + // A method's invocant is already in callArgs. CODE callbacks + // receive self only when their argspec explicitly requests it. + eventData = buildEventDataFromArgspec( + argspec, eventName, eventArgs, self, methodCallback, pstate); + for (int idx = 0; idx < eventData.size(); idx++) { + RuntimeArray.push(callArgs, eventData.get(idx)); + } + } else { + for (RuntimeScalar arg : eventArgs) RuntimeArray.push(callArgs, arg); } - } else { - for (RuntimeScalar arg : eventArgs) { - RuntimeArray.push(callArgs, arg); + if (methodCallback) { + int blessId = RuntimeScalarType.blessedId(self); + String className = blessId != 0 + ? NameNormalizer.getBlessStr(blessId) : "HTML::Parser"; + RuntimeScalar method = InheritanceResolver.findMethodInHierarchy( + cb.toString(), className, null, 0); + if (method != null) RuntimeCode.apply(method, callArgs, RuntimeContextType.VOID); + } else { + RuntimeCode.apply(cb, callArgs, RuntimeContextType.VOID); } + } finally { + // These are owned native call buffers, including when callback + // dispatch fails or a method is absent. Escaped @_ references + // keep their own aggregate owner and remain usable. + MortalList.scopeExitCleanupArray(callArgs); + MortalList.scopeExitCleanupArray(eventData); + MortalList.flushAboveMark(); } - RuntimeCode.apply(cb, callArgs, RuntimeContextType.VOID); } } @@ -679,7 +675,7 @@ private static RuntimeArray buildEventDataFromArgspec(String argspec, String eve RuntimeArray.push(result, eventArgs[1]); } else { // Return empty hash ref for non-start events - RuntimeArray.push(result, new RuntimeHash().createReference()); + RuntimeArray.push(result, new RuntimeHash().createAnonymousReference()); } break; @@ -688,7 +684,7 @@ private static RuntimeArray buildEventDataFromArgspec(String argspec, String eve if ("start".equals(eventName) && eventArgs.length > 2) { RuntimeArray.push(result, eventArgs[2]); } else { - RuntimeArray.push(result, new RuntimeArray().createReference()); + RuntimeArray.push(result, new RuntimeArray().createAnonymousReference()); } break; @@ -834,7 +830,7 @@ private static RuntimeArray buildEventDataFromArgspec(String argspec, String eve // declaration => [declaration_body] // process => [pi_body] RuntimeArray.push(result, - buildTokensArray(eventName, eventArgs).createReference()); + buildTokensArray(eventName, eventArgs).createAnonymousReference()); break; case "tokenpos": @@ -852,9 +848,9 @@ private static RuntimeArray buildEventDataFromArgspec(String argspec, String eve RuntimeArray pair = new RuntimeArray(); RuntimeArray.push(pair, new RuntimeScalar(0)); RuntimeArray.push(pair, new RuntimeScalar(0)); - RuntimeArray.push(pos, pair.createReference()); + RuntimeArray.push(pos, pair.createAnonymousReference()); } - RuntimeArray.push(result, pos.createReference()); + RuntimeArray.push(result, pos.createAnonymousReference()); } break; @@ -1311,8 +1307,8 @@ private static void parseHtml(RuntimeScalar self, RuntimeHash selfHash, RuntimeH if (!ignoreElement) { fireEvent(self, selfHash, pstate, "start", new RuntimeScalar(tagName), - attrs.createReference(), - attrSeq.createReference(), + attrs.createAnonymousReference(), + attrSeq.createAnonymousReference(), new RuntimeScalar(origText)); } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java b/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java index c3a1d3afd4..2179504f06 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java @@ -386,9 +386,18 @@ private static void replaceCaptureTracking( && code.capturedScalars != null) { for (int i = 0; i < code.capturedScalars.length; i++) { if (code.capturedScalars[i] == previous) { - scalar.retainClosureCapture(); - previous.releaseClosureCapture(); + PerlOwnerSlot replacementOwner = scalar.retainClosureCaptureSlot(code); + PerlOwnerSlot[] ownerSlots = code.capturedScalarOwnerSlots; + PerlOwnerSlot previousOwner = ownerSlots != null + && i < ownerSlots.length + ? ownerSlots[i] : null; + ((RuntimeScalar) previous).releaseClosureCaptureSlot(previousOwner); code.capturedScalars[i] = scalar; + if (ownerSlots == null || ownerSlots.length != code.capturedScalars.length) { + ownerSlots = new PerlOwnerSlot[code.capturedScalars.length]; + code.capturedScalarOwnerSlots = ownerSlots; + } + ownerSlots[i] = replacementOwner; return; } } @@ -399,7 +408,7 @@ private static void replaceCaptureTracking( for (int i = 0; i < code.capturedAggregates.length; i++) { if (code.capturedAggregates[i] == previous) { PerlOwnerSlot replacementOwner = - replacement.retainClosureCaptureOwner(); + replacement.retainClosureCaptureOwner(code); PerlOwnerSlot[] ownerSlots = code.capturedAggregateOwnerSlots; PerlOwnerSlot previousOwner = ownerSlots != null && i < ownerSlots.length @@ -416,8 +425,72 @@ private static void replaceCaptureTracking( } } - if (previous != null) previous.releaseClosureCapture(); - replacement.retainClosureCapture(); + if (previous instanceof RuntimeScalar previousScalar + && replacement instanceof RuntimeScalar replacementScalar) { + previousScalar.releaseClosureCapture(); + appendScalarCapture(code, replacementScalar); + return; + } + if (previous != null && replacement != null) { + previous.releaseClosureCapture(); + appendAggregateCapture(code, replacement); + } + } + + private static void appendScalarCapture(RuntimeCode code, RuntimeScalar replacement) { + RuntimeScalar[] existing = code.capturedScalars == null + ? new RuntimeScalar[0] : code.capturedScalars; + PerlOwnerSlot[] existingSlots = code.capturedScalarOwnerSlots; + java.util.ArrayList captures = new java.util.ArrayList<>( + java.util.Arrays.asList(existing)); + java.util.ArrayList slots = new java.util.ArrayList<>(existing.length); + for (int i = 0; i < existing.length; i++) { + PerlOwnerSlot slot = existingSlots != null && i < existingSlots.length + ? existingSlots[i] : null; + if (slot == null || !slot.isActive()) { + RuntimeScalar captured = existing[i]; + if (captured.captureCount > captured.ownerSlotCount( + PerlOwnerSlot.Kind.SCALAR_CAPTURE)) { + captured.releaseClosureCapture(); + } + slot = captured.retainClosureCaptureSlot(code); + } + slots.add(slot); + } + if (!captures.contains(replacement)) { + captures.add(replacement); + slots.add(replacement.retainClosureCaptureSlot(code)); + } + code.capturedScalars = captures.toArray(new RuntimeScalar[0]); + code.capturedScalarOwnerSlots = slots.toArray(new PerlOwnerSlot[0]); + } + + private static void appendAggregateCapture(RuntimeCode code, RuntimeBase replacement) { + RuntimeBase[] existing = code.capturedAggregates == null + ? new RuntimeBase[0] : code.capturedAggregates; + PerlOwnerSlot[] existingSlots = code.capturedAggregateOwnerSlots; + java.util.ArrayList captures = new java.util.ArrayList<>( + java.util.Arrays.asList(existing)); + java.util.ArrayList slots = new java.util.ArrayList<>(existing.length); + for (int i = 0; i < existing.length; i++) { + PerlOwnerSlot slot = existingSlots != null && i < existingSlots.length + ? existingSlots[i] : null; + if (slot == null || !slot.isActive()) { + RuntimeBase captured = existing[i]; + if (captured.captureCount > captured.ownerSlotCount( + PerlOwnerSlot.Kind.CLOSURE_PAD)) { + captured.releaseClosureCapture(); + } + slot = captured.retainClosureCaptureOwner(code); + } + slots.add(slot); + } + if (!captures.contains(replacement)) { + captures.add(replacement); + slots.add(replacement.retainClosureCaptureOwner(code)); + } + code.capturedAggregates = captures.toArray(new RuntimeBase[0]); + code.capturedAggregateOwnerSlots = slots.toArray(new PerlOwnerSlot[0]); } /** @@ -492,12 +565,20 @@ public static RuntimeList svRefcount(RuntimeArray args, int ctx) { if (base != null) { int rc = base.refCount; if (rc == Integer.MIN_VALUE) return new RuntimeScalar(0).getList(); - int nativeCaptureOwners = base.nativeCaptureOwnerCount(); + boolean compileHintsHash = base == GlobalVariable.getGlobalHash( + GlobalContext.encodeSpecialVar("H")); + int nativeOwners = base.nativeOwnerSlotCount(); + if (compileHintsHash) { + // The package-root identity records the strong table edge for + // lifecycle cleanup, but Perl's SvREFCNT helper deliberately + // hides this special global slot from the reported count. + nativeOwners -= base.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT); + } if (rc < 0) { - if (nativeCaptureOwners == 0) return new RuntimeScalar(1).getList(); // untracked - rc = nativeCaptureOwners; + if (nativeOwners == 0) return new RuntimeScalar(1).getList(); // untracked + rc = nativeOwners; } else { - rc += nativeCaptureOwners; + rc += nativeOwners; } // An ampersand-style call may pass a weak qr// scalar directly // (qr-72922's refcount_is helper does this). The call-frame alias @@ -541,8 +622,6 @@ public static RuntimeList svRefcount(RuntimeArray args, int ctx) { // runtime, but Perl's core refcount helper does not expose that // bookkeeping slot as a lexical pad owner. Its helper also // compensates for the ampersand-call argument alias below. - boolean compileHintsHash = base == GlobalVariable.getGlobalHash( - GlobalContext.encodeSpecialVar("H")); int extra = (base.localBindingExists && !compileHintsHash ? 1 : 0) + base.foreachAliasCount; if (rc == 2 @@ -695,29 +774,14 @@ public static RuntimeList jperlOwnerTrace(RuntimeArray args, int ctx) { return new RuntimeScalar("NOT_REF").getList(); } - /** - * Phase 4 (refcount_alignment_plan.md): Run a reachability sweep from - * Perl roots (globals, rescued objects) and clear weak refs for - * unreachable objects. Returns the number of weak refs cleared. This - * is jperl-only; under native Perl it should be treated as a no-op. - */ + /** Compatibility stub: PerlOnJava uses owner release, not a reachability sweep. */ public static RuntimeList jperl_gc(RuntimeArray args, int ctx) { - // Two passes: the first pass fires DESTROY on unreachable - // objects, which may break circular refs and make more objects - // unreachable. The second pass catches those cascades. - int cleared = ReachabilityWalker.sweepWeakRefs(); - int secondPass = ReachabilityWalker.sweepWeakRefs(); - return new RuntimeScalar(cleared + secondPass).getList(); + return new RuntimeScalar(0).getList(); } - /** - * Quiet statement-boundary sweep. Unlike jperl_gc(), this does not drain - * rescued DBIC-style objects before END-time cleanup. - */ + /** Compatibility stub for the removed quiet reachability sweep. */ public static RuntimeList jperl_gc_quiet(RuntimeArray args, int ctx) { - int cleared = ReachabilityWalker.sweepWeakRefs(true); - int secondPass = ReachabilityWalker.sweepWeakRefs(true); - return new RuntimeScalar(cleared + secondPass).getList(); + return new RuntimeScalar(0).getList(); } /** @@ -729,18 +793,9 @@ public static RuntimeList jperl_freetmps(RuntimeArray args, int ctx) { return new RuntimeScalar(0).getList(); } - /** - * Reconcile destroyable objects that require deterministic DESTROY side - * effects but are not weak-ref referents, without clearing arbitrary weak - * refs and without forcing HotSpot GC. Used by DBI statement polling to - * match Perl's cursor DESTROY/finish timing. - */ + /** Compatibility stub: destroyable objects are released through their owners. */ public static RuntimeList jperl_sweep_destroyables_no_jvm_gc(RuntimeArray args, int ctx) { - int destroyed = ReachabilityWalker.sweepDestroyableObjects(false); - if (destroyed > 0) { - destroyed += ReachabilityWalker.sweepDestroyableObjects(false); - } - return new RuntimeScalar(destroyed).getList(); + return new RuntimeScalar(0).getList(); } /** diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java b/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java index 9e0bb4a2f9..3b6469dc77 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java @@ -255,58 +255,9 @@ public static RuntimeList isweak(RuntimeArray args, int ctx) { } RuntimeScalar ref = args.get(0); boolean wasWeak = WeakRefRegistry.isweak(ref); - if (wasWeak - && DestroyDispatch.hasRescuedObjects() - && isLeakTracerWeakRegistryCleanupCheck() - && !ModuleInitGuard.inModuleInit()) { - // DBIC is asking whether one registry slot is weak while other - // unrelated objects can still form intentional strong cycles. - // Drain the pending DESTROY rescues explicitly, then use quiet - // sweeps so those cycle islands retain Perl refcount semantics. - // A full non-quiet sweep is intentionally aggressive and would - // clear DBIC's own shifted diagnostic reference mid-expression. - DestroyDispatch.clearRescuedWeakRefs(); - ReachabilityWalker.sweepWeakRefs(true); - ReachabilityWalker.sweepWeakRefs(true); - return new RuntimeScalar(true).getList(); - } return new RuntimeScalar(wasWeak).getList(); } - private static boolean isLeakTracerWeakRegistryCleanupCheck() { - for (int i = 0; i < 8; i++) { - RuntimeCode code = RuntimeCode.getActiveCodeAt(i); - if (code == null) break; - String filename = code.cvStartFile; - String packageName = code.packageName; - if ("DBICTest::Util::LeakTracer".equals(packageName) - || (filename != null && filename.endsWith("DBICTest/Util/LeakTracer.pm"))) { - String subName = code.subName; - if (subName == null || !subName.endsWith("assert_empty_weakregistry")) { - continue; - } - RuntimeArray args = RuntimeCode.getActiveArgsAt(i); - if (args == null || args.size() == 0) return false; - RuntimeScalar registryRef = args.get(0); - if (!RuntimeScalarType.isReference(registryRef) - || !(registryRef.value instanceof RuntimeHash registry)) { - return false; - } - // t/52leaks uses a large outer registry and a one-entry inner - // diagnostic. Quiet END checks are also outer cleanup points, - // even when a test registered only one schema or storage. - return registry.size() > 5 - || (args.size() > 1 && args.get(1).getBoolean()); - } - } - return false; - } - - // isweak() normally reports weak-ref metadata only. DBIC's LeakTracer is a - // narrow compatibility exception: real Perl would already have cleared the - // rescued weak slots it is inspecting, so we drain those rescues and run - // cycle-preserving quiet sweeps while returning the pre-sweep weak status. - /** * Dualvar functionality. * diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegexCallback.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegexCallback.java index b9141f639b..8014465e23 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegexCallback.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegexCallback.java @@ -1,6 +1,7 @@ package org.perlonjava.runtime.regex; import org.perlonjava.runtime.runtimetypes.RuntimeCode; +import org.perlonjava.runtime.runtimetypes.PerlOwnerSlot; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; import org.perlonjava.runtime.operators.WarnDie; @@ -27,6 +28,7 @@ boolean isBlock() { final String source; final boolean uninitializedWarningsEnabled; private int ownerCount; + private PerlOwnerSlot codeOwnerSlot; private RuntimeRegexCallback( RuntimeCode code, Kind kind, String sourceLocation, String lexicalPackage, @@ -40,19 +42,25 @@ private RuntimeRegexCallback( } synchronized void retainOwner() { - if (code.refCount < 0) return; - if (ownerCount++ == 0) { - code.refCount++; + if (ownerCount++ != 0) return; + if (code.refCount >= 0) code.refCount++; + if (code.refCount != Integer.MIN_VALUE) { + codeOwnerSlot = new PerlOwnerSlot(PerlOwnerSlot.Kind.REGEX_CALLBACK); + codeOwnerSlot.acquireNativeOwner(code); } } synchronized void releaseOwner() { - if (code.refCount < 0) return; if (ownerCount <= 0) return; ownerCount--; if (ownerCount != 0) return; + if (codeOwnerSlot != null) { + codeOwnerSlot.release(); + codeOwnerSlot = null; + } if (code.refCount > 0) code.refCount--; - if (code.refCount == 0 && code.stashRefCount <= 0) { + if (code.refCount != Integer.MIN_VALUE && code.stashRefCount <= 0 + && !code.hasPerlOwnerForCaptureOtherThan(null)) { code.releaseCaptures(); } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/AutovivificationArray.java b/src/main/java/org/perlonjava/runtime/runtimetypes/AutovivificationArray.java index dc8b750c16..fc4f1fc9d1 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/AutovivificationArray.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/AutovivificationArray.java @@ -47,7 +47,10 @@ public static void vivify(RuntimeArray array) { RuntimeScalar referent = arrayProxy.scalarToAutovivify; if (referent instanceof RuntimeBaseProxy proxy) { - proxy.set(array.createReference()); + // This array is owned through the scalar slot being + // autovivified. It has no separate named @array binding; + // the scalar slot's counted owner keeps it alive. + proxy.set(array.createAnonymousReference()); } else { referent.value = array; referent.type = RuntimeScalarType.ARRAYREFERENCE; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/DestroyDispatch.java b/src/main/java/org/perlonjava/runtime/runtimetypes/DestroyDispatch.java index 9b6049b2eb..19a9de120d 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/DestroyDispatch.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/DestroyDispatch.java @@ -42,6 +42,7 @@ static void markDestroyTargetRescued() { } static void requestSweepAfterOuterDestroy() { + if (!ReachabilityWalker.correctiveSweepsEnabled()) return; state().sweepPendingAfterOuterDestroy = true; } @@ -210,7 +211,19 @@ public static void invalidateCache() { * @param referent the RuntimeBase object to destroy */ public static void callDestroy(RuntimeBase referent) { + callDestroy(referent, false); + } + + static void callDestroyFromCountedOwner(RuntimeBase referent) { + callDestroy(referent, true); + } + + private static void callDestroy(RuntimeBase referent, boolean finalCountedOwner) { // refCount is already MIN_VALUE (set by caller) + if (referent instanceof RuntimeCode code && code.hasActiveCodeOwner()) { + code.deferFinalReleaseToActiveCode(); + return; + } // Shared canonical objects carry a published class name across // runtimes, but numeric bless IDs are runtime-local. Destruction may @@ -218,10 +231,19 @@ public static void callDestroy(RuntimeBase referent) { // in a child, so resolve the published name before method lookup. referent.synchronizePublishedSharedBlessing(); - // A shared aggregate may have runtime-local fetch views in another - // ithread. Canonical deletion waits for those views; the runtime that - // releases the last view performs the one Perl DESTROY callback. - if (referent.deferSharedDestroy()) return; + // A shared aggregate may have runtime-local fetch views. Canonical + // deletion waits for those views; an otherwise-unowned fetch proxy + // still clears weak references to its local Perl identity without + // cascading into shared backing storage. + RuntimeBase.SharedDestroyDisposition sharedDisposition = + referent.sharedDestroyDisposition(); + if (sharedDisposition == RuntimeBase.SharedDestroyDisposition.DEFER) return; + if (sharedDisposition == RuntimeBase.SharedDestroyDisposition.CLEAR_LOCAL_VIEW) { + referent.beginPerlDestruction(); + WeakRefRegistry.clearWeakRefsTo(referent, finalCountedOwner); + referent.markPerlDestroyed(); + return; + } // The explicit lifecycle is authoritative once final cleanup has // completed. Keep refCount's sentinel interpretation at the legacy @@ -280,20 +302,20 @@ public static void callDestroy(RuntimeBase referent) { return; } referent.beginPerlDestruction(); - WeakRefRegistry.clearWeakRefsTo(referent); + WeakRefRegistry.clearWeakRefsTo(referent, finalCountedOwner); + releaseTiedContainerOwner(referent); if (referent instanceof RuntimeHash hash) { - MortalList.scopeExitCleanupHash(hash); + MortalList.scopeExitCleanupDestroyedHash(hash); MortalList.flush(); } else if (referent instanceof RuntimeArray arr) { - MortalList.scopeExitCleanupArray(arr); + MortalList.scopeExitCleanupDestroyedArray(arr); MortalList.flush(); } else if (referent instanceof RuntimeScalar scalar) { - RuntimeScalar.scopeExitCleanup(scalar); + RuntimeScalar.scopeExitCleanupDestroyed(scalar); + MortalList.flush(); + } else if (referent instanceof RuntimeCode code) { + code.releaseStateStorage(); MortalList.flush(); - } - if ((referent instanceof RuntimeHash || referent instanceof RuntimeArray) - && MortalList.requestWeakSweepsForDestroyedContainer(referent)) { - MortalList.requestImmediateWeakSweep(); } referent.markPerlDestroyed(); return; @@ -309,7 +331,7 @@ public static void callDestroy(RuntimeBase referent) { // prematurely would cascade to clear weak references (e.g., in Sub::Defer's // %DEFERRED hash), causing infinite recursion in Moo/DBIx::Class. if (referent instanceof RuntimeCode code) { - if (code.stashRefCount <= 0) { + if (code.hasCountedCodeSlots || code.stashRefCount <= 0) { // A queued END block is an owning CV slot even though it is // not installed in a stash. Selective refCount can transiently // reach zero while an assertion or callback releases another @@ -324,7 +346,7 @@ public static void callDestroy(RuntimeBase referent) { code.rescuedActiveEndBlockOwner = true; return; } - if (ReachabilityWalker.strongCycleRetainsWeakReferent(code)) { + if (!finalCountedOwner && ReachabilityWalker.strongCycleRetainsWeakReferent(code)) { code.refCount = 1; return; } @@ -348,15 +370,17 @@ public static void callDestroy(RuntimeBase referent) { // Unblessed object — clear weak refs immediately and cascade into elements // to decrement refCounts of any tracked references they hold. referent.beginPerlDestruction(); - WeakRefRegistry.clearWeakRefsTo(referent); + WeakRefRegistry.clearWeakRefsTo(referent, finalCountedOwner); + releaseTiedContainerOwner(referent); if (referent instanceof RuntimeHash hash) { - MortalList.scopeExitCleanupHash(hash); + MortalList.scopeExitCleanupDestroyedHash(hash); } else if (referent instanceof RuntimeArray arr) { - MortalList.scopeExitCleanupArray(arr); + MortalList.scopeExitCleanupDestroyedArray(arr); } else if (referent instanceof RuntimeScalar scalar) { - RuntimeScalar.scopeExitCleanup(scalar); + RuntimeScalar.scopeExitCleanupDestroyed(scalar); + } else if (referent instanceof RuntimeCode code) { + code.releaseStateStorage(); } - MortalList.requestWeakSweepsForDestroyedContainer(referent); referent.markPerlDestroyed(); return; } @@ -393,19 +417,19 @@ private static void doCallDestroy(RuntimeBase referent, String className) { // Without this, blessed objects without DESTROY (e.g., Moo objects like // DBIx::Class::Storage::BlockRunner) leak their contained references. WeakRefRegistry.clearWeakRefsTo(referent); + releaseTiedContainerOwner(referent); if (referent instanceof RuntimeHash hash) { - MortalList.scopeExitCleanupHash(hash); + MortalList.scopeExitCleanupDestroyedHash(hash); MortalList.flush(); } else if (referent instanceof RuntimeArray arr) { - MortalList.scopeExitCleanupArray(arr); + MortalList.scopeExitCleanupDestroyedArray(arr); MortalList.flush(); } else if (referent instanceof RuntimeScalar scalar) { - RuntimeScalar.scopeExitCleanup(scalar); + RuntimeScalar.scopeExitCleanupDestroyed(scalar); + MortalList.flush(); + } else if (referent instanceof RuntimeCode code) { + code.releaseStateStorage(); MortalList.flush(); - } - if ((referent instanceof RuntimeHash || referent instanceof RuntimeArray) - && MortalList.requestWeakSweepsForDestroyedContainer(referent)) { - MortalList.requestImmediateWeakSweep(); } referent.markPerlDestroyed(); return; @@ -471,6 +495,7 @@ private static void doCallDestroy(RuntimeBase referent, String className) { referent.refCount = 0; } + boolean callbackCompleted = false; try { // Build $self reference to pass as $_[0] RuntimeScalar self = new RuntimeScalar(); @@ -536,105 +561,9 @@ private static void doCallDestroy(RuntimeBase referent, String className) { } } - // Reblessing an object while its DESTROY method is running starts - // a new destruction lifecycle for the new class. The original - // one-shot guard belongs to the old class, so do not let it - // suppress the new class's DESTROY (op/ref.t's _B -> A case). - // Invoke it directly while the object is still protected by the - // current DESTROY frame; going through callDestroy() here would - // be treated as re-entrance and discarded. - if (referent.blessId != destroyBlessId) { - String reblessedClassName = NameNormalizer.getBlessStr(referent.blessId); - // A same-class bless may receive a fresh runtime-local ID after - // method-cache invalidation. It does not start a new Perl - // destruction lifecycle: cache implementations such as - // Class::Std::Fast use it while retaining the object. Compare - // the Perl class names rather than their ephemeral IDs. - if (reblessedClassName != null && !reblessedClassName.isEmpty() - && !reblessedClassName.equals(className)) { - referent.destroyFired = false; - doCallDestroy(referent, reblessedClassName); - return; - } - } - - // Phase 3: Resurrection detection. If refCount > 0 at this point, - // a strong ref to the object escaped DESTROY (e.g. Devel::StackTrace- - // like @DB::args capture into a persistent array, or Schema-style - // self-save). Mark needsReDestroy and let the next decrement-to-0 - // re-invoke DESTROY. Don't clear weak refs or cascade — the object - // is still alive. - if (hasEscapedPerlOwner(referent, checkingGlobalOwnerDelta, - activeOwnersAtStart, ownerSlotsAtStart) - && !state.destroyTargetRescued) { - warnIfResurrectedDuringGlobalDestruction(referent, className); - referent.needsReDestroy = true; - return; - } - - // Check if DESTROY rescued the object by storing $self somewhere. - // If destroyTargetRescued was set during DESTROY (detected by - // RuntimeScalar.setLargeRefCounted when the old value was a weak ref - // to currentDestroyTarget being overwritten by a strong ref to the - // same target), the object should survive — skip cascade cleanup. - // - // Example: Schema::DESTROY re-attaches itself to a ResultSource via - // $source->{schema} = $self - // This triggers rescue detection because the old value ($source->{schema}, - // a weak ref to Schema) is being replaced by a strong ref to Schema. - if (state.destroyTargetRescued) { - if ("DESTRUCT".equals(GlobalVariable.getGlobalVariable( - GlobalContext.GLOBAL_PHASE).toString())) { - warnIfResurrectedDuringGlobalDestruction(referent, className); - } - // Object was rescued by DESTROY (e.g., Schema::DESTROY self-save). - // - // refCount has been set to 1 by setLargeRefCounted during rescue - // detection (MIN_VALUE → 1). This represents the rescue container's - // single counted reference (e.g., $source->{schema} = $self). - // - // When the rescue source eventually dies and its DESTROY weakens - // source->{schema}, refCount goes 1→0→callDestroy. That callDestroy - // is intercepted by the rescuedObjects check (skip cleanup), keeping - // Schema's internals intact during the phantom chain. Proper cleanup - // happens later via processRescuedObjects at block scope exit. - // - // Keep destroyFired=true to prevent infinite DESTROY loops. - // - // Don't clear weak refs here — the rescued object is still alive, - // and other sources may still have weak refs to it that need to - // remain defined until the object truly dies. - // - // Don't cascade — the rescued object's internal fields (Storage, - // DBI::db, ResultSources) must remain intact because the object - // is still alive. - // - // Track rescued objects so clearRescuedWeakRefs can clean up - // at END time. - state.rescuedObjects.add(referent); - MortalList.invalidateExternalRootSnapshot(); - return; - } - - // Object was NOT rescued — clear weak refs now (deferred from callDestroy). - // In Perl 5, weak refs are cleared after DESTROY only if the object - // wasn't resurrected. We match that by clearing here. - WeakRefRegistry.clearWeakRefsTo(referent); - - // Cascading destruction: after DESTROY runs and the object was NOT rescued, - // walk the destroyed object's internal fields for any blessed references - // and defer their refCount decrements. This ensures nested objects - // (e.g., $self->{inner}) are destroyed when their parent is destroyed. - if (referent instanceof RuntimeHash hash) { - MortalList.scopeExitCleanupHash(hash); - MortalList.flush(); - } else if (referent instanceof RuntimeArray arr) { - MortalList.scopeExitCleanupArray(arr); - MortalList.flush(); - } else if (referent instanceof RuntimeScalar scalar) { - RuntimeScalar.scopeExitCleanup(scalar); - MortalList.flush(); - } + callbackCompleted = true; + finishDestroyOwnership(referent, className, destroyBlessId, + checkingGlobalOwnerDelta, activeOwnersAtStart, ownerSlotsAtStart); } catch (Exception e) { String msg = e.getMessage(); if (msg == null) msg = e.getClass().getName(); @@ -650,11 +579,18 @@ private static void doCallDestroy(RuntimeBase referent, String className) { && dieException.getWarningBits() != null ? dieException.getWarningBits() : RuntimeCode.getWarningBitsForCode(code); - if (!(e instanceof PerlDieException dieException - && dieException.isMiscWarningSuppressed())) { - WarnDie.warnWithCategoryFromCode( - new RuntimeScalar(warning), - new RuntimeScalar(""), "misc", warningBits); + try { + if (!(e instanceof PerlDieException dieException + && dieException.isMiscWarningSuppressed())) { + WarnDie.warnWithCategoryFromCode( + new RuntimeScalar(warning), + new RuntimeScalar(""), "misc", warningBits); + } + } finally { + if (!callbackCompleted) { + finishDestroyOwnership(referent, className, destroyBlessId, + checkingGlobalOwnerDelta, activeOwnersAtStart, ownerSlotsAtStart); + } } } finally { if (referent.needsReDestroy || state.destroyTargetRescued @@ -677,18 +613,69 @@ private static void doCallDestroy(RuntimeBase referent, String className) { // Without this, die inside DESTROY would clobber the caller's $@. dollarAt.type = savedDollarAt.type; dollarAt.value = savedDollarAt.value; - // Phase D: outermost DESTROY is finishing. If any nested set() - // released a cyclic blessed-with-DESTROY ref, fire one walker - // sweep to clear any now-orphaned weak refs. This amortizes - // the sweep cost to at most one per top-level DESTROY instead - // of per-set(). Gated by ModuleInitGuard to avoid tripping - // during require/use load. - if (savedTarget == null && state.sweepPendingAfterOuterDestroy - && !ModuleInitGuard.inModuleInit()) { - state.sweepPendingAfterOuterDestroy = false; - ReachabilityWalker.sweepWeakRefs(false, false); + } + } + + /** Final destruction releases magic even when an expression still borrows the container. */ + private static void releaseTiedContainerOwner(RuntimeBase referent) { + if (referent instanceof RuntimeHash hash && hash.elements instanceof TieHash tieHash) { + tieHash.releaseTiedObject(); + } else if (referent instanceof RuntimeArray array && array.elements instanceof TieArray tieArray) { + tieArray.releaseTiedObject(); + } + } + + /** The callback's success or failure does not change its ownership obligations. */ + private static void finishDestroyOwnership( + RuntimeBase referent, String className, int destroyBlessId, + boolean checkingGlobalOwnerDelta, Set activeOwnersAtStart, + Set ownerSlotsAtStart) { + LifecycleRuntimeState state = state(); + if (referent.blessId != destroyBlessId) { + String reblessedClassName = NameNormalizer.getBlessStr(referent.blessId); + // Compare names: a same-class bless can receive a new runtime-local + // ID without starting a new destruction lifecycle. Dispatch directly + // while the current DESTROY frame still guards callDestroy reentry. + if (reblessedClassName != null && !reblessedClassName.isEmpty() + && !reblessedClassName.equals(className)) { + referent.destroyFired = false; + doCallDestroy(referent, reblessedClassName); + return; + } + } + if (hasEscapedPerlOwner(referent, checkingGlobalOwnerDelta, + activeOwnersAtStart, ownerSlotsAtStart) && !state.destroyTargetRescued) { + referent.needsReDestroy = true; + warnIfResurrectedDuringGlobalDestruction(referent, className); + return; + } + if (state.destroyTargetRescued) { + if ("DESTRUCT".equals(GlobalVariable.getGlobalVariable( + GlobalContext.GLOBAL_PHASE).toString())) { + warnIfResurrectedDuringGlobalDestruction(referent, className); } + // A weak slot promoted to a rescuing strong owner keeps the + // object's contents and observers until its rescue lifetime ends. + state.rescuedObjects.add(referent); + MortalList.invalidateExternalRootSnapshot(); + return; + } + WeakRefRegistry.clearWeakRefsTo(referent); + releaseTiedContainerOwner(referent); + if (referent instanceof RuntimeHash hash) { + MortalList.scopeExitCleanupDestroyedHash(hash); + } else if (referent instanceof RuntimeArray array) { + MortalList.scopeExitCleanupDestroyedArray(array); + } else if (referent instanceof RuntimeScalar scalar) { + RuntimeScalar.scopeExitCleanupDestroyed(scalar); + } else if (referent instanceof RuntimeCode code) { + // Blessed CVs may resurrect in DESTROY. Their state pad remains + // intact through the callback and the resurrection decision. + code.releaseStateStorage(); + } else { + return; } + MortalList.flush(); } /** @@ -714,7 +701,8 @@ private static boolean hasEscapedPerlOwner(RuntimeBase referent, && ReachabilityWalker.isScalarReachable(owner)) return true; } for (PerlOwnerSlot ownerSlot : referent.ownerSlotSnapshot()) { - if (!ownerSlotsAtStart.contains(ownerSlot)) return true; + if ((ownerSlot.isNativeOwner() || ownerSlot.kind() == PerlOwnerSlot.Kind.CLOSURE_PAD) + && !ownerSlotsAtStart.contains(ownerSlot)) return true; } return false; } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ExecutionRuntimeState.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ExecutionRuntimeState.java index 87c4d3a9e0..fb27b54272 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ExecutionRuntimeState.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ExecutionRuntimeState.java @@ -72,8 +72,10 @@ public final class ExecutionRuntimeState { public final Deque activeEvalSources = new ArrayDeque<>(); public final ArrayDeque> syntheticCallerFrames = new ArrayDeque<>(); public final Deque argsStack = new ArrayDeque<>(); - /** Weak roots for JVM call arguments while their expressions are evaluated. */ - final ArrayList> callArgumentFrames = + final Deque> argumentAliasOwnerFrames = + new ArrayDeque<>(); + /** Explicit owners for staged arguments, retired by call completion or frame exit. */ + final ArrayList callArgumentFrames = new ArrayList<>(); public final Deque activeCodeStack = new ArrayDeque<>(); /** Perl source callsites aligned with the active Perl code frames. */ @@ -98,6 +100,8 @@ public final class ExecutionRuntimeState { public Object reusableActiveLexicalFrame; /** Lexical cells owned by the top-level compilation unit. */ public final Map topLevelLexicals = new LinkedHashMap<>(); + /** Owner identities for aggregate cells held by top-level lexical names. */ + final Map topLevelLexicalOwnerSlots = new LinkedHashMap<>(); public final Deque> pristineArgsStack = new ArrayDeque<>(); /** Reusable one-scalar return lists, populated only after scalar extraction. */ final Deque availableScalarResultLists = new ArrayDeque<>(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeState.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeState.java index 962308a5c1..90b2ed54db 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeState.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeState.java @@ -24,8 +24,11 @@ public final class GlobalRuntimeState { private static final AtomicInteger NEXT_THREAD_COMPILED_CODE_REF_BASE = new AtomicInteger(COMPILED_CODE_REF_RANGE_SIZE); private final Map scalarValues = new HashMap<>(); + private final Map packageScalarOwnerSlots = new HashMap<>(); private final Map arrayValues = new HashMap<>(); + private final Map packageArrayOwnerSlots = new HashMap<>(); private final Map hashValues = new HashMap<>(); + private final Map packageHashOwnerSlots = new HashMap<>(); private final Map foreachScalarAliases = new HashMap<>(); private final Map temporaryScalarAliases = new HashMap<>(); private final Map importedSubs = new HashMap<>(); @@ -89,6 +92,67 @@ public Map scalarValues() { return scalarValues; } + Map packageScalarOwnerSlots() { + return packageScalarOwnerSlots; + } + + void rebuildPackageScalarOwnerSlots() { + for (PerlOwnerSlot ownerSlot : packageScalarOwnerSlots.values()) { + RuntimeScalar previous = ownerSlot.referent() instanceof RuntimeScalar scalar + ? scalar : null; + ownerSlot.release(); + if (previous != null) previous.refreshUntrackedReferenceOwner(); + } + packageScalarOwnerSlots.clear(); + for (Map.Entry entry : scalarValues.entrySet()) { + RuntimeScalar scalar = entry.getValue(); + if (scalar == null) continue; + PerlOwnerSlot ownerSlot = new PerlOwnerSlot( + PerlOwnerSlot.Kind.PACKAGE_SCALAR_SLOT); + ownerSlot.acquirePackageScalarIdentity(scalar); + packageScalarOwnerSlots.put(entry.getKey(), ownerSlot); + scalar.refreshUntrackedReferenceOwner(); + } + } + + Map packageArrayOwnerSlots() { + return packageArrayOwnerSlots; + } + + Map packageHashOwnerSlots() { + return packageHashOwnerSlots; + } + + void rebuildPackageAggregateOwnerSlots() { + releasePackageAggregateOwnerSlots(packageArrayOwnerSlots, false); + releasePackageAggregateOwnerSlots(packageHashOwnerSlots, false); + rebuildPackageAggregateOwnerSlots(arrayValues, packageArrayOwnerSlots); + rebuildPackageAggregateOwnerSlots(hashValues, packageHashOwnerSlots); + } + + private static void rebuildPackageAggregateOwnerSlots( + Map values, + Map slots) { + for (Map.Entry entry : values.entrySet()) { + RuntimeBase aggregate = entry.getValue(); + if (aggregate == null) continue; + PerlOwnerSlot ownerSlot = new PerlOwnerSlot( + PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT); + ownerSlot.acquirePackageAggregateIdentity(aggregate); + slots.put(entry.getKey(), ownerSlot); + } + } + + private static void releasePackageAggregateOwnerSlots( + Map slots, boolean finalRelease) { + for (PerlOwnerSlot ownerSlot : slots.values()) { + RuntimeBase aggregate = ownerSlot.referent(); + ownerSlot.release(); + if (finalRelease) WeakRefRegistry.onStrongOwnerSlotReleased(aggregate); + } + slots.clear(); + } + long scalarBindingVersion() { return scalarBindingVersion.get(); } @@ -279,6 +343,7 @@ void cachedStashEnumerationVersion(long version) { synchronized int registerCompiledCodeRef(RuntimeScalar ref) { int id = nextCompiledCodeRefId++; + if (ref != null) ref.retainCodeSlotBinding(); compiledCodeRefs.put(id, ref); return id; } @@ -329,6 +394,7 @@ void noteCodeRefMutation(RuntimeScalar previous, RuntimeScalar replacement) { private static boolean isCodeRefEdgeRoot(RuntimeScalar scalar) { if (scalar == null) return false; return !(scalar.value instanceof RuntimeCode code) + || code.retiredOwnedEdges || ReachabilityWalker.hasWalkableCodeEdges(code, true); } @@ -388,6 +454,18 @@ public void resetCoreGlobalsInitialization() { void clearCoreValues() { scalarValues.clear(); + for (PerlOwnerSlot ownerSlot : packageScalarOwnerSlots.values()) { + RuntimeScalar scalar = ownerSlot.referent() instanceof RuntimeScalar value + ? value : null; + ownerSlot.release(); + if (scalar != null) { + scalar.refreshUntrackedReferenceOwner(); + WeakRefRegistry.onStrongOwnerSlotReleased(scalar); + } + } + packageScalarOwnerSlots.clear(); + releasePackageAggregateOwnerSlots(packageArrayOwnerSlots, true); + releasePackageAggregateOwnerSlots(packageHashOwnerSlots, true); invalidateScalarBindingCache(); arrayValues.clear(); hashValues.clear(); @@ -455,9 +533,11 @@ synchronized void snapshotInto(GlobalRuntimeState target, RuntimeGraphCloner clo cloneMap(foreachScalarAliases, target.foreachScalarAliases, cloner, RuntimeScalar.class); cloneMap(temporaryScalarAliases, target.temporaryScalarAliases, cloner, RuntimeScalar.class); cloneMap(codeRefs, target.codeRefs, cloner, RuntimeScalar.class); + cloner.restoreCodeSlotBindings(target.codeRefs.values()); cloneMap(pseudoConstants, target.pseudoConstants, cloner, RuntimeScalar.class); cloneMap(compactStashValues, target.compactStashValues, cloner, RuntimeScalar.class); cloneMap(pinnedCodeRefs, target.pinnedCodeRefs, cloner, RuntimeScalar.class); + cloner.restoreCodeSlotBindings(target.pinnedCodeRefs.values()); // Parsers register many inert glob placeholders (notably through eval). // Cloning all of them into every ithread makes snapshot cost quadratic // for regex matrices that compile thousands of evals. Only an IO slot @@ -474,6 +554,7 @@ synchronized void snapshotInto(GlobalRuntimeState target, RuntimeGraphCloner clo target.compiledCodeRefs.put(entry.getKey(), (RuntimeScalar) cloner.cloneValue(entry.getValue())); } + cloner.restoreCodeSlotBindings(target.compiledCodeRefs.values()); target.importedSubs.putAll(importedSubs); target.subsPragmaDeclarations.putAll(subsPragmaDeclarations); @@ -525,6 +606,9 @@ synchronized void snapshotCompiledCodeRefsInto( + entry.getKey()); } target.compiledCodeRefs.put(entry.getKey(), cloned); + if (existing == null && cloned != null) { + cloner.restoreCodeSlotBindings(java.util.List.of(cloned)); + } } target.nextCompiledCodeRefId = Math.max(target.nextCompiledCodeRefId, nextCompiledCodeRefId); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java index 7edd678928..9535e31e3f 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java @@ -332,6 +332,94 @@ private void noteScalarBindingMutation() { } } + private boolean ownsPackageScalarSlots(GlobalRuntimeState state) { + return (Object) table.apply(state) == state.scalarValues(); + } + + private Map packageAggregateOwnerSlots( + GlobalRuntimeState state) { + Object values = table.apply(state); + if (values == state.arrayValues()) return state.packageArrayOwnerSlots(); + if (values == state.hashValues()) return state.packageHashOwnerSlots(); + return null; + } + + private void updatePackageScalarOwner( + GlobalRuntimeState state, String key, RuntimeScalar nextScalar) { + if (!ownsPackageScalarSlots(state)) return; + Map slots = state.packageScalarOwnerSlots(); + PerlOwnerSlot slot = slots.get(key); + RuntimeScalar previousScalar = slot != null + && slot.referent() instanceof RuntimeScalar previous ? previous : null; + if (nextScalar == null) { + if (slot != null) { + slots.remove(key, slot); + slot.release(); + } + } else { + if (slot == null) { + slot = new PerlOwnerSlot(PerlOwnerSlot.Kind.PACKAGE_SCALAR_SLOT); + slots.put(key, slot); + } + slot.acquirePackageScalarIdentity(nextScalar); + } + if (previousScalar != null && previousScalar != nextScalar) { + previousScalar.refreshUntrackedReferenceOwner(); + WeakRefRegistry.onStrongOwnerSlotReleased(previousScalar); + } + if (nextScalar != null) nextScalar.refreshUntrackedReferenceOwner(); + } + + private void updatePackageAggregateOwner( + GlobalRuntimeState state, String key, RuntimeBase nextAggregate) { + Map slots = packageAggregateOwnerSlots(state); + if (slots == null) return; + PerlOwnerSlot slot = slots.get(key); + RuntimeBase previousAggregate = slot == null ? null : slot.referent(); + if (nextAggregate == null) { + if (slot != null) { + slots.remove(key, slot); + slot.release(); + WeakRefRegistry.onStrongOwnerSlotReleased(previousAggregate); + } + return; + } + if (slot == null) { + slot = new PerlOwnerSlot(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT); + slots.put(key, slot); + } + slot.acquirePackageAggregateIdentity(nextAggregate); + if (previousAggregate != null && previousAggregate != nextAggregate) { + WeakRefRegistry.onStrongOwnerSlotReleased(previousAggregate); + } + } + + private void releasePackageScalarOwners(GlobalRuntimeState state) { + if (!ownsPackageScalarSlots(state)) return; + Map slots = state.packageScalarOwnerSlots(); + for (Map.Entry entry : new ArrayList<>(slots.entrySet())) { + RuntimeScalar scalar = entry.getValue().referent() instanceof RuntimeScalar value + ? value : null; + entry.getValue().release(); + if (scalar != null) { + scalar.refreshUntrackedReferenceOwner(); + WeakRefRegistry.onStrongOwnerSlotReleased(scalar); + } + } + slots.clear(); + } + + private void releasePackageAggregateOwners(GlobalRuntimeState state) { + Map slots = packageAggregateOwnerSlots(state); + if (slots == null) return; + for (PerlOwnerSlot ownerSlot : new ArrayList<>(slots.values())) { + RuntimeBase aggregate = ownerSlot.referent(); + ownerSlot.release(); + WeakRefRegistry.onStrongOwnerSlotReleased(aggregate); + } + slots.clear(); + } + @Override public T get(Object key) { return delegate().get(key); @@ -359,10 +447,14 @@ public boolean isEmpty() { @Override public T put(String key, T value) { + GlobalRuntimeState state = globalState(); boolean newKey = !delegate().containsKey(key); markStashEntryVisible(key); markPackageGlobalRoot(value); T previous = delegate().put(key, value); + updatePackageScalarOwner(state, key, + value instanceof RuntimeScalar scalar ? scalar : null); + updatePackageAggregateOwner(state, key, value); noteScalarBindingMutation(); if (newKey) { invalidateStashEnumerationCache(); @@ -376,6 +468,10 @@ public T putIfAbsent(String key, T value) { T previous = delegate().get(key); if (previous != null) { markPackageGlobalRoot(previous); + GlobalRuntimeState state = globalState(); + updatePackageScalarOwner(state, key, + previous instanceof RuntimeScalar scalar ? scalar : null); + updatePackageAggregateOwner(state, key, previous); return previous; } return put(key, value); @@ -383,8 +479,13 @@ public T putIfAbsent(String key, T value) { @Override public T remove(Object key) { + GlobalRuntimeState state = globalState(); T previous = delegate().remove(key); if (previous != null) { + if (key == null || key instanceof String) { + updatePackageScalarOwner(state, (String) key, null); + updatePackageAggregateOwner(state, (String) key, null); + } noteScalarBindingMutation(); invalidateStashEnumerationCache(); invalidatePackageRootSnapshot(); @@ -394,23 +495,29 @@ public T remove(Object key) { @Override public void clear() { - if (!delegate().isEmpty()) { + GlobalRuntimeState state = globalState(); + boolean hadEntries = !delegate().isEmpty(); + delegate().clear(); + releasePackageScalarOwners(state); + releasePackageAggregateOwners(state); + if (hadEntries) { noteScalarBindingMutation(); invalidateStashEnumerationCache(); invalidatePackageRootSnapshot(); } - delegate().clear(); } @Override public Set> entrySet() { - Map entries = delegate(); + GlobalRuntimeState ownerState = globalState(); + Map entries = table.apply(ownerState); return new AbstractSet<>() { @Override public Iterator> iterator() { Iterator> iterator = entries.entrySet().iterator(); return new Iterator<>() { private boolean canRemove; + private String lastKey; @Override public boolean hasNext() { return iterator.hasNext(); } @@ -419,6 +526,7 @@ public Entry next() { Entry current = iterator.next(); canRemove = true; String key = current.getKey(); + lastKey = key; return new SimpleEntry<>(key, current.getValue()) { @Override public T setValue(T value) { @@ -433,7 +541,10 @@ public T setValue(T value) { public void remove() { if (!canRemove) throw new IllegalStateException("next() has not been called"); iterator.remove(); + updatePackageScalarOwner(ownerState, lastKey, null); + updatePackageAggregateOwner(ownerState, lastKey, null); canRemove = false; + lastKey = null; noteScalarBindingMutation(); invalidateStashEnumerationCache(); invalidatePackageRootSnapshot(); @@ -475,6 +586,7 @@ public RuntimeScalar put(String key, RuntimeScalar value) { markPackageGlobalRoot(value); RuntimeScalar old = delegate().put(key, value); if (old != value) { + if (value != null) value.retainCodeSlotBinding(); noteCodeRefMutation(old, value); } if (newKey) { @@ -491,6 +603,7 @@ public RuntimeScalar put(String key, RuntimeScalar value) { } } maybeInvalidateMethodCacheForCodeRefPut(key, old, value); + if (old != null && old != value) old.releaseCodeSlotBinding(); return old; } @@ -516,11 +629,13 @@ public RuntimeScalar remove(Object key) { if (key instanceof String s) { InheritanceResolver.invalidateMethodLookupCachesForStashSubKey(s); } + if (prev != null) prev.releaseCodeSlotBinding(); return prev; } @Override public void clear() { + java.util.List previous = new java.util.ArrayList<>(delegate().values()); if (!isEmpty()) { invalidateCodeRefNames(); invalidateStashEnumerationCache(); @@ -534,6 +649,9 @@ public void clear() { invalidatePackageRootSnapshot(); } delegate().clear(); + for (RuntimeScalar slot : previous) { + if (slot != null) slot.releaseCodeSlotBinding(); + } } @Override public RuntimeScalar get(Object key) { return delegate().get(key); } @@ -580,6 +698,7 @@ public void remove() { invalidateStashEnumerationCache(); invalidatePackageRootSnapshot(); InheritanceResolver.invalidateMethodLookupCachesForStashSubKey(key); + if (value != null) value.releaseCodeSlotBinding(); } }; } @@ -748,6 +867,10 @@ static T markPackageGlobalRoot(T root) { } for (RuntimeScalar value : hash.elements.values()) { hash.markPackageRootedValue(value); + if (value != null) { + value.markPrepopulatedPackageAggregateOwner(hash); + value.refreshUntrackedReferenceOwner(); + } } } else if (root instanceof RuntimeArray array) { // Package-root marking may race with another thread mutating a @@ -756,11 +879,27 @@ static T markPackageGlobalRoot(T root) { // fail-fast ConcurrentModificationException into Perl code. for (RuntimeScalar value : array.elements.toArray(RuntimeScalar[]::new)) { array.markPackageRootedValue(value); + if (value != null) { + value.markPrepopulatedPackageAggregateOwner(array); + value.refreshUntrackedReferenceOwner(); + } } } return root; } + /** Whether an aggregate is installed in a package slot, including direct map writes. */ + static boolean isPackageRootedAggregate(RuntimeBase value) { + if (PerlRuntime.currentOrNull() == null) return false; + if (value instanceof RuntimeArray array) { + return array.isPackageGlobalRoot || globalArrays.containsValue(array); + } + if (value instanceof RuntimeHash hash) { + return hash.isPackageRootedHash() || globalHashes.containsValue(hash); + } + return false; + } + static void invalidatePackageRootSnapshot() { MortalList.invalidateExternalRootSnapshot(); } @@ -1607,7 +1746,8 @@ public static void aliasGlobalHash(String key, RuntimeHash hash) { /** Replace a package CODE slot with the supplied code reference. */ public static void aliasGlobalCodeRef(String key, RuntimeScalar codeRef) { - globalCodeRefs.put(key, codeRef); + // CODE refaliasing aliases the CV, not the RHS scalar variable. + globalCodeRefs.put(key, new RuntimeScalar(codeRef)); } /** @@ -2250,7 +2390,7 @@ public static RuntimeScalar getGlobalCodeRef(String key) { } RuntimeScalar resolvedVar = globalCodeRefs.get(resolvedKey); if (resolvedVar != null) { - pinnedCodeRefs().put(resolvedKey, resolvedVar); + pinCodeRef(resolvedKey, resolvedVar); return resolvedVar; } } @@ -2274,7 +2414,7 @@ public static RuntimeScalar getGlobalCodeRef(String key) { } // Pin the RuntimeScalar so it survives stash deletion - pinnedCodeRefs().put(key, var); + pinCodeRef(key, var); return var; } @@ -2400,7 +2540,7 @@ public static RuntimeScalar getGlobalCodeRefForFreshLookup(String key) { RuntimeScalar resolvedVar = globalCodeRefs.get(resolvedKey); if (resolvedVar != null) { if (!deletedCodeRefPins().contains(resolvedKey)) { - pinnedCodeRefs().put(resolvedKey, resolvedVar); + pinCodeRef(resolvedKey, resolvedVar); } return resolvedVar; } @@ -2417,7 +2557,7 @@ public static RuntimeScalar getGlobalCodeRefForFreshLookup(String key) { markPackageGlobalRoot(var); } if (!deletedCodeRefPins().contains(key)) { - pinnedCodeRefs().put(key, var); + pinCodeRef(key, var); } return var; } @@ -2456,11 +2596,10 @@ public static RuntimeScalar getGlobalCodeRefForNamedReference(String key) { if (slot == null) { globalCodeRefs.put(alias, shared); } else { - slot.type = RuntimeScalarType.CODE; - slot.value = sharedCode; + slot.set(new RuntimeScalar(sharedCode)); } if (!deletedCodeRefPins().contains(alias)) { - pinnedCodeRefs().put(alias, globalCodeRefs.get(alias)); + pinCodeRef(alias, globalCodeRefs.get(alias)); } } markPackageGlobalRoot(shared); @@ -2606,7 +2745,7 @@ public static RuntimeScalar defineGlobalCodeRef(String key) { markPackageGlobalRoot(ref); } deletedCodeRefPins().remove(resolvedKey); - pinnedCodeRefs().put(resolvedKey, ref); + pinCodeRef(resolvedKey, ref); return ref; } @@ -2643,22 +2782,40 @@ public static boolean existsGlobalCodeRef(String key) { * @param key The glob name key. * @param codeRef The new RuntimeScalar to pin (typically a new empty one). */ + private static void pinCodeRef(String key, RuntimeScalar codeRef) { + RuntimeScalar previous = pinnedCodeRefs().get(key); + if (previous == codeRef) return; + if (codeRef != null) codeRef.retainCodeSlotBinding(); + pinnedCodeRefs().put(key, codeRef); + if (previous != null) previous.releaseCodeSlotBinding(); + } + static void replacePinnedCodeRef(String key, RuntimeScalar codeRef) { if (pinnedCodeRefs().containsKey(key)) { - pinnedCodeRefs().put(key, codeRef); + pinCodeRef(key, codeRef); } } static void enterLocalizedCodeRef(String key, RuntimeScalar displacedCodeRef) { localizedCodeRefDepth().merge(key, 1, Integer::sum); if (displacedCodeRef != null) { + displacedCodeRef.retainCodeSlotBinding(); displacedLocalizedCodeRefs().put(displacedCodeRef, key); } } + static boolean hasLocalizedCodeRef(RuntimeCode code) { + if (code == null) return false; + for (RuntimeScalar displacedCodeRef : displacedLocalizedCodeRefs().keySet()) { + if (displacedCodeRef != null && displacedCodeRef.value == code) return true; + } + return false; + } + static void exitLocalizedCodeRef(String key, RuntimeScalar displacedCodeRef) { if (displacedCodeRef != null) { displacedLocalizedCodeRefs().remove(displacedCodeRef); + displacedCodeRef.releaseCodeSlotBinding(); } Integer depth = localizedCodeRefDepth().get(key); if (depth == null) { @@ -2806,6 +2963,7 @@ public static RuntimeScalar removeGlobalCodeRefForStashDelete(String key) { if (removedCode.stashRefCount > 0) { removedCode.stashRefCount--; } + removedCode.noteStashOwnerReleased(); } return deleted; } @@ -2842,7 +3000,11 @@ public static RuntimeScalar deleteGlobalCodeRefAsScalar(RuntimeScalar key, Strin * @param prefix The namespace prefix (e.g., "Foo::") to clear. */ public static void clearPinnedCodeRefsForNamespace(String prefix) { - pinnedCodeRefs().keySet().removeIf(k -> k.startsWith(prefix)); + for (String key : new java.util.ArrayList<>(pinnedCodeRefs().keySet())) { + if (!key.startsWith(prefix)) continue; + RuntimeScalar removed = pinnedCodeRefs().remove(key); + if (removed != null) removed.releaseCodeSlotBinding(); + } deletedCodeRefPins().removeIf(k -> k.startsWith(prefix)); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/LifecycleRuntimeState.java b/src/main/java/org/perlonjava/runtime/runtimetypes/LifecycleRuntimeState.java index 1e1390b7a5..c3269fa014 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/LifecycleRuntimeState.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/LifecycleRuntimeState.java @@ -24,6 +24,9 @@ final class LifecycleRuntimeState { // Parallel to pending. Weak provenance permits a tail-call handoff to // drain only the decrement whose owning argument it actually replaces. final ArrayList> pendingOwnerScalars = new ArrayList<>(); + // Parallel to pending. Captured when a scalar decrement is queued so a + // tail-call handoff can match its source argument frame by identity. + final ArrayList pendingArgumentFrames = new ArrayList<>(); // Parallel to pending. A non-scalar transient owner kind is recorded only // for trace attribution; it never changes runtime reachability. final ArrayList pendingTransientOwnerKinds = new ArrayList<>(); @@ -31,6 +34,7 @@ final class LifecycleRuntimeState { new ArrayList<>(); final ArrayList pendingTiedReleases = new ArrayList<>(); final ArrayList pendingIoReleases = new ArrayList<>(); + final ArrayList pendingUntrackedReturnOwnerReleases = new ArrayList<>(); final ArrayList deferredCaptures = new ArrayList<>(); final IdentityHashMap deferredCapturesSet = new IdentityHashMap<>(); final Set deferredScopeExitCaptureCandidates = @@ -45,22 +49,14 @@ final class LifecycleRuntimeState { int[] marks = new int[INITIAL_MARK_CAPACITY]; int[] tiedReleaseMarks = new int[INITIAL_MARK_CAPACITY]; int[] ioReleaseMarks = new int[INITIAL_MARK_CAPACITY]; + int[] untrackedReturnOwnerMarks = new int[INITIAL_MARK_CAPACITY]; int markDepth; boolean flushing; int reachabilityDrainDepth; int topLevelFlushDepth; - // Start within the configured quiet-sweep throttle window. A zero value - // would trigger a full root walk at the first statement boundary, often - // between callbacks that are expected to dispatch together. - long lastAutoSweepNanos = System.nanoTime(); - boolean inAutoSweep; /** Suppress sweeps while clone hooks and the first child entry build Perl roots. */ int weakSweepSuppressionDepth; boolean immediateWeakSweepRequested; - boolean deferCaptureReachabilityToBoundarySweep; - final ArrayList deferredCaptureSweepCandidates = new ArrayList<>(); - long boundaryWeakSweepGeneration; - long deferredCaptureSweepGeneration; final Set targetedWeakSweepReferents = Collections.newSetFromMap(new IdentityHashMap<>()); Set flushReachableCache; @@ -79,16 +75,19 @@ final class LifecycleRuntimeState { weakSweepRootWitnesses = new IdentityHashMap<>(); ReachabilityQueryStats activeReachabilityQueryStats; - void pushMortalMark(int pendingSize, int tiedReleaseSize, int ioReleaseSize) { + void pushMortalMark(int pendingSize, int tiedReleaseSize, int ioReleaseSize, + int untrackedReturnOwnerReleaseSize) { if (markDepth == marks.length) { int capacity = marks.length * 2; marks = Arrays.copyOf(marks, capacity); tiedReleaseMarks = Arrays.copyOf(tiedReleaseMarks, capacity); ioReleaseMarks = Arrays.copyOf(ioReleaseMarks, capacity); + untrackedReturnOwnerMarks = Arrays.copyOf(untrackedReturnOwnerMarks, capacity); } marks[markDepth] = pendingSize; tiedReleaseMarks[markDepth] = tiedReleaseSize; ioReleaseMarks[markDepth] = ioReleaseSize; + untrackedReturnOwnerMarks[markDepth] = untrackedReturnOwnerReleaseSize; markDepth++; } @@ -139,9 +138,14 @@ void clear() { pending.clear(); pendingOwnerReleases.clear(); pendingOwnerScalars.clear(); + pendingArgumentFrames.clear(); pendingTransientOwnerKinds.clear(); pendingTiedReleases.clear(); pendingIoReleases.clear(); + for (RuntimeScalar scalar : pendingUntrackedReturnOwnerReleases) { + if (scalar != null) scalar.releaseMortalReturnValueOwner(); + } + pendingUntrackedReturnOwnerReleases.clear(); deferredCaptures.clear(); deferredCapturesSet.clear(); deferredScopeExitCaptureCandidates.clear(); @@ -155,17 +159,11 @@ void clear() { flushing = false; reachabilityDrainDepth = 0; topLevelFlushDepth = 0; - lastAutoSweepNanos = System.nanoTime(); - inAutoSweep = false; fullRootSnapshot = null; unblessedAggregateRootSnapshot = null; weakSweepRootWitnesses.clear(); activeReachabilityQueryStats = null; immediateWeakSweepRequested = false; - deferCaptureReachabilityToBoundarySweep = false; - deferredCaptureSweepCandidates.clear(); - boundaryWeakSweepGeneration = 0; - deferredCaptureSweepGeneration = 0; targetedWeakSweepReferents.clear(); flushReachableCache = null; flushTiedReachableCache = null; @@ -194,4 +192,5 @@ void clear() { scalarRegistry.clear(); scalarRegisterStacks.clear(); } + } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java b/src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java index 3f496e2b6f..6c8e4d2f64 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java @@ -26,16 +26,13 @@ static final class DeferredArgumentOwnerRelease { final RuntimeBase base; final RuntimeBase.PendingOwnerRelease ownerRelease; final String transientOwnerKind; - final java.lang.ref.WeakReference ownerScalar; DeferredArgumentOwnerRelease(RuntimeBase base, RuntimeBase.PendingOwnerRelease ownerRelease, - String transientOwnerKind, - RuntimeScalar ownerScalar) { + String transientOwnerKind) { this.base = base; this.ownerRelease = ownerRelease; this.transientOwnerKind = transientOwnerKind; - this.ownerScalar = new java.lang.ref.WeakReference<>(ownerScalar); } } @@ -99,11 +96,8 @@ private static void refreshBoundaryWork(LifecycleRuntimeState state) { boolean needed = !state.pending.isEmpty() || !state.pendingTiedReleases.isEmpty() || !state.pendingIoReleases.isEmpty() - || state.deferredCapturesMayBeReady - || state.immediateWeakSweepRequested - || !state.targetedWeakSweepReferents.isEmpty() - || !state.statementBoundaryDestroyableObjects.isEmpty() - || state.weakRefsExist; + || !state.pendingUntrackedReturnOwnerReleases.isEmpty() + || state.deferredCapturesMayBeReady; if (!needed && state.boundaryWorkRegistered.compareAndSet(true, false)) { runtimesWithBoundaryWork.decrementAndGet(); } @@ -172,7 +166,11 @@ private static boolean directlyReferences(RuntimeBase value, RuntimeBase target, && !WeakRefRegistry.isweak(scalar) && (scalar.type & RuntimeScalarType.REFERENCE_BIT) != 0 && scalar.value instanceof RuntimeBase referent) { - return directlyReferences(referent, target, visited); + // The reference owns its referent through the slot ledger. Its + // descendants are owned by their own element slots; searching + // through that graph here duplicates reference counting and can + // traverse all Moose metadata on every lexical scope exit. + return referent == target; } if (value instanceof RuntimeList list) { for (RuntimeBase element : list.elements) { @@ -182,10 +180,6 @@ private static boolean directlyReferences(RuntimeBase value, RuntimeBase target, for (RuntimeScalar element : array.elements) { if (directlyReferences(element, target, visited)) return true; } - } else if (value instanceof RuntimeHash hash) { - for (RuntimeScalar element : hash.elements.values()) { - if (directlyReferences(element, target, visited)) return true; - } } return false; } @@ -256,6 +250,7 @@ public static void deferDecrementFromArraySlot( markBoundaryWork(state); RuntimeBase.PendingOwnerRelease ownerRelease = base.queueOwnerRelease(ownerScalar, releaseSite); + base.releaseActiveOwner(ownerScalar); queueDeferredBase(state, base, ownerRelease, null, ownerScalar); } @@ -289,6 +284,8 @@ private static void queueDeferredBase(LifecycleRuntimeState state, RuntimeBase b state.pendingOwnerReleases.add(ownerRelease); state.pendingOwnerScalars.add(ownerScalar == null ? null : new java.lang.ref.WeakReference<>(ownerScalar)); + state.pendingArgumentFrames.add( + RuntimeCode.argumentAliasFrameForOwner(ownerScalar)); state.pendingTransientOwnerKinds.add(transientOwnerKind); } @@ -306,6 +303,14 @@ public static void deferIoOwnerRelease(RuntimeScalar scalar) { state.pendingIoReleases.add(scalar); } + /** Queue an untracked mortal return slot for its caller's normal boundary. */ + static void deferUntrackedReturnValueOwnerRelease(RuntimeScalar scalar) { + if (scalar == null || scalar.untrackedReferenceOwnerSlot() == null) return; + LifecycleRuntimeState state = state(); + markBoundaryWork(state); + state.pendingUntrackedReturnOwnerReleases.add(scalar); + } + /** * Record a captured scalar whose scope has exited but whose refCount * could not be decremented because {@code captureCount > 0}. @@ -321,75 +326,13 @@ public static void addDeferredCapture(RuntimeScalar scalar) { markBoundaryWork(state); state.deferredCaptures.add(scalar); state.deferredCapturesSet.merge(scalar, 1, Integer::sum); - noteDeferredScopeExitCaptureCandidate(state, scalar); if (scalar.captureCount == 0 && scalar.scopeExited) { state.deferredCapturesMayBeReady = true; } } + /** Compatibility hook; capture ownership no longer needs a reachability candidate. */ public static void noteDeferredScopeExitCaptureCandidate(RuntimeScalar scalar) { - if (scalar == null || !scalar.scopeExited - || (scalar.type & RuntimeScalarType.REFERENCE_BIT) == 0 - || !(scalar.value instanceof RuntimeBase base) - || !isDeferredScopeExitCandidate(base)) return; - noteDeferredScopeExitCaptureCandidate(state(), scalar); - } - - private static void noteDeferredScopeExitCaptureCandidate( - LifecycleRuntimeState state, RuntimeScalar scalar) { - if (scalar.value instanceof RuntimeBase base && isDeferredScopeExitCandidate(base)) { - state.deferredScopeExitCaptureCandidates.add(scalar); - } - } - - private static boolean isDeferredScopeExitCandidate(RuntimeBase base) { - if (base == null || base.blessId == 0) return false; - if (WeakRefRegistry.hasWeakRefsTo(base)) return true; - String className = NameNormalizer.getBlessStr(base.blessId); - return className != null && DestroyDispatch.classHasDestroy(base.blessId, className); - } - - /** - * Release eval STRING over-captures whose declaring scope just exited and - * which cannot be reached by live code or an END block. Reachability sweeps - * also perform this check, but waiting for their cadence can keep request - * objects alive across unrelated calls (and defer their cleanup until - * global destruction). - */ - private static void processUnreachableScopeExitCaptureCandidates( - LifecycleRuntimeState state) { - if (state.deferredScopeExitCaptureCandidates.isEmpty()) return; - - Set endReachable = ReachabilityWalker.walkEndBlockRoots(); - Set cycleProtected = ReachabilityWalker.strongCycleProtectedReferents(); - boolean found = false; - for (RuntimeScalar scalar : new ArrayList<>(state.deferredScopeExitCaptureCandidates)) { - if (!scalar.scopeExited) continue; - RuntimeBase base = scalar.value instanceof RuntimeBase value ? value : null; - boolean scalarReachable = ReachabilityWalker.isScalarReachable(scalar) - || ReachabilityWalker.isReachableFromRoots(scalar); - boolean retained = endReachable.contains(scalar) - || (base != null && endReachable.contains(base)) - || (base != null && cycleProtected.contains(base)) - || scalarReachable; - if (retained) continue; - - scalar.releaseUnreachableSemanticCaptureOwner(); - Integer deferredCount = state.deferredCapturesSet.get(scalar); - if (deferredCount != null) { - for (int i = state.deferredCaptures.size() - 1; i >= 0; i--) { - if (state.deferredCaptures.get(i) != scalar) continue; - deferDecrementIfTracked(scalar); - state.deferredCaptures.remove(i); - removeFromDeferredSet(scalar); - } - } else if (base != null) { - requestTargetedWeakSweep(base); - } - found = true; - } - state.deferredScopeExitCaptureCandidates.clear(); - if (found) flushAboveMark(); } static void noteDeferredCaptureMayBeReady() { @@ -474,57 +417,10 @@ public static void flushDeferredCaptures() { // decrements have been processed and before global destruction. DestroyDispatch.clearRescuedWeakRefs(); - // Final sweep: clear weak refs for ALL remaining blessed objects. - // At this point the main script has returned and all lexical scopes - // have exited. Some objects may still have inflated selective - // refCounts (due to JVM temporaries, method-call copies, interpreter - // captures) that prevent DESTROY from firing. Their weak refs would - // remain defined forever, causing DBIC's leak tracer to report false - // leaks. Clearing weak refs here is safe because: - // 1. Only weak refs are cleared — the Java objects remain alive - // 2. CODE refs are excluded (they may still be called from stashes) - // 3. END blocks have completed, so real closure captures are no longer needed - WeakRefRegistry.clearAllBlessedWeakRefs(); } - /** - * Release scope-exited captures that cannot be reached by an END block or - * a package CODE slot, while retaining genuine END-visible captures for - * the unconditional post-END drain in {@link #flushDeferredCaptures()}. - * - * This preserves Perl's ordering: unrelated file lexicals are destroyed - * before END, but values captured by code that END can invoke stay alive - * until END has completed. - */ + /** Compatibility hook; captured owners now leave through explicit slots. */ public static void flushDeferredCapturesBeforeEnd() { - LifecycleRuntimeState state = state(); - Set endReachable = ReachabilityWalker.walkEndBlockRoots(); - ReachabilityWalker.releaseUnreachableScopeExitedCaptureOwners( - WeakRefRegistry.snapshotWeakRefReferents(), endReachable); - boolean found = false; - for (int i = state.deferredCaptures.size() - 1; i >= 0; i--) { - RuntimeScalar scalar = state.deferredCaptures.get(i); - boolean retained = endReachable.contains(scalar) - || (scalar.value instanceof RuntimeBase base && endReachable.contains(base)); - if (retained) continue; - - // This scope-exited pad was captured by eval STRING, but no END - // block can reach it. Its semantic capture edge must not keep a - // blessed referent alive through global destruction merely because - // the compiled eval object is still Java-reachable. - scalar.releaseUnreachableSemanticCaptureOwner(); - deferDecrementIfTracked(scalar); - state.deferredCaptures.remove(i); - removeFromDeferredSet(scalar); - found = true; - } - if (found) flush(); - - // Weak-reference leak tracers commonly run from END. Selective - // refcounts can remain inflated by JVM temporaries even after all Perl - // owners have gone away, so clear only referents outside the semantic - // END graph now. Retained captures are cleared by the post-END drain. - WeakRefRegistry.clearBlessedWeakRefsExcept(endReachable); } /** @@ -535,6 +431,7 @@ public static void flushDeferredCapturesBeforeEnd() { */ public static void deferDecrementIfTracked(RuntimeScalar scalar) { if (!isActive() || scalar == null) return; + if (scalar.hasCodeSlotBinding()) return; if (!scalar.refCountOwned) return; if (((scalar.type & RuntimeScalarType.REFERENCE_BIT) != 0 || scalar.type == RuntimeScalarType.GLOB) @@ -544,11 +441,11 @@ public static void deferDecrementIfTracked(RuntimeScalar scalar) { if (base.refCountTrace) { base.traceRefCount(0, "MortalList.deferDecrementIfTracked (queued, scalar.refCountOwned->false)"); } - base.releaseActiveOwner(scalar); LifecycleRuntimeState state = state(); markBoundaryWork(state); RuntimeBase.PendingOwnerRelease ownerRelease = base.queueOwnerRelease( scalar, "MortalList.deferDecrementIfTracked"); + base.releaseActiveOwner(scalar); queueDeferredBase(state, base, ownerRelease, null, scalar); } else if (base.refCount == 0 && base.clearedOwnedAggregateElement @@ -564,7 +461,7 @@ public static void deferDecrementIfTracked(RuntimeScalar scalar) { base.releaseOwner(scalar, "deferDecrementIfTracked zero-count owner"); } base.releaseActiveOwner(scalar); - requestImmediateWeakSweep(); + requestCorrectiveWeakSweep(); } // Note: WEAKLY_TRACKED (-2) objects are NOT scheduled for destruction // on scope exit. We can't count strong refs for non-DESTROY objects @@ -591,7 +488,8 @@ public static void releaseCapturedDecrement(RuntimeScalar scalar) { */ public static void deferDecrementIfNotCaptured(RuntimeScalar scalar) { if (!isActive() || scalar == null) return; - if (scalar.captureCount > 0) { + if (scalar.captureCount > 0 + || (scalar.referencedByScalarReference && scalar.localBindingExists)) { // Delegate to scopeExitCleanup which handles: // - Self-referential cycle detection (eval STRING closures) // - Setting scopeExited flag for deferred cleanup via releaseCaptures @@ -611,8 +509,13 @@ public static void deferDecrementIfNotCaptured(RuntimeScalar scalar) { * Never-stored blessed objects (refCount == 0) are bumped to ensure DESTROY fires. */ public static void deferDestroyForContainerClear(Iterable elements) { - if (!isActive()) return; + if (elements == null) return; + boolean active = isActive(); for (RuntimeScalar scalar : elements) { + if (scalar != null && scalar.untrackedReferenceOwnerSlot() != null) { + scalar.refreshUntrackedReferenceOwner(); + } + if (!active) continue; // Use the recursive path so a discarded wrapper/container also // releases references stored in its fields. Test::Deep's // temporary comparator is a blessed hash whose `val` field can @@ -651,6 +554,14 @@ public static void evalExceptionScopeCleanup(Object local) { * arrays/hashes). Called at scope exit for {@code my %hash} variables. */ public static void scopeExitCleanupHash(RuntimeHash hash) { + scopeExitCleanupHash(hash, false); + } + + static void scopeExitCleanupDestroyedHash(RuntimeHash hash) { + scopeExitCleanupHash(hash, true); + } + + private static void scopeExitCleanupHash(RuntimeHash hash, boolean finalRelease) { if (!isActive() || hash == null) return; // An aggregate borrowed through @_ belongs to its caller. Its // current frame can end while the caller's lexical is still live; @@ -658,21 +569,28 @@ public static void scopeExitCleanupHash(RuntimeHash hash) { // Clearing it here // makes Internals::SvREFCNT under-report the caller's reference and // lets Const::Fast freeze the original instead of its clone. - if (RuntimeCode.deferCleanupForActiveArgumentAggregate(hash)) return; + if (!finalRelease && RuntimeCode.deferCleanupForActiveArgumentAggregate(hash)) return; + scopeExitCleanupHashBody(hash, finalRelease); + } + + private static void scopeExitCleanupHashBody(RuntimeHash hash, boolean finalRelease) { // Clear localBindingExists: the named variable's scope is ending. // This allows subsequent refCount==0 events (from setLargeRefCounted // or flush) to correctly trigger callDestroy, since the local // variable no longer holds a strong reference. boolean hadLocalBinding = hash.localBindingExists; hash.localBindingExists = false; - if (hash.captureCount > 0) { + hash.releaseUnownedClosureCaptures(); + if (!finalRelease && hash.captureCount > 0) { hash.scopeExited = true; return; } // A live reference keeps the container and its IO element slots alive. - if (hash.refCount > 0 || temporaryRootDirectlyReferences(hash)) return; + if (!finalRelease && (hash.refCount > 0 || hash.hasOwnerSlots() + || temporaryRootDirectlyReferences(hash))) return; for (RuntimeScalar val : hash.elements.values()) { RuntimeScalar.releaseIoOwner(val); + if (val != null) val.releaseContainerOwner(hash); } // Skip container walks only when there are NO blessed objects AND NO // weak refs anywhere in the JVM. If weak refs exist (even to unblessed @@ -723,7 +641,7 @@ public static void scopeExitCleanupHash(RuntimeHash hash) { // source_registrations hash is assigned into the live schema while the // original hash later exits; walking it here would destroy ResultSources // still reachable through the schema. - if (hadLocalBinding + if (!finalRelease && hadLocalBinding && hash.refCount >= 0 && hash.hadCountedReference && isReachableFromExternalRootCached(hash)) return; @@ -745,6 +663,14 @@ public static void scopeExitCleanupArray(RuntimeArray arr) { /** Scope-exit cleanup with a returned aggregate whose IO aliases must survive. */ public static void scopeExitCleanupArray(RuntimeArray arr, RuntimeBase returned) { + scopeExitCleanupArray(arr, returned, false); + } + + static void scopeExitCleanupDestroyedArray(RuntimeArray arr) { + scopeExitCleanupArray(arr, null, true); + } + + private static void scopeExitCleanupArray(RuntimeArray arr, RuntimeBase returned, boolean finalRelease) { if (!isActive() || arr == null) return; // A threads::shared array has canonical storage that outlives each // runtime-local wrapper. Its elements are released by mutations of @@ -753,19 +679,25 @@ public static void scopeExitCleanupArray(RuntimeArray arr, RuntimeBase returned) // See scopeExitCleanupHash: an active @_ alias does not end the // caller's lexical lifetime. In particular, Const::Fast uses // SvREFCNT to decide whether it must clone a referenced aggregate. - if (RuntimeCode.deferCleanupForActiveArgumentAggregate(arr)) return; + if (!finalRelease && RuntimeCode.deferCleanupForActiveArgumentAggregate(arr)) return; + scopeExitCleanupArrayBody(arr, returned, finalRelease); + } + + private static void scopeExitCleanupArrayBody( + RuntimeArray arr, RuntimeBase returned, boolean finalRelease) { // Clear localBindingExists: the named variable's scope is ending. // This allows subsequent refCount==0 events (from setLargeRefCounted // or flush) to correctly trigger callDestroy, since the local // variable no longer holds a strong reference. arr.localBindingExists = false; - if (arr.captureCount > 0) { + arr.releaseUnownedClosureCaptures(); + if (!finalRelease && arr.captureCount > 0) { arr.scopeExited = true; return; } // Alias arrays such as @_ do not own their original element slots. - if (arr.refCount > 0 || temporaryRootDirectlyReferences(arr) - || directlyReferences(returned, arr)) return; + if (!finalRelease && (arr.refCount > 0 || arr.hasOwnerSlots() + || temporaryRootDirectlyReferences(arr) || directlyReferences(returned, arr))) return; // \$#array creates a scalar proxy, not an array reference. Once the // lexical AV exits, that proxy becomes an orphaned undef lvalue; it // must not retain this Java RuntimeArray merely through its backing @@ -775,11 +707,13 @@ public static void scopeExitCleanupArray(RuntimeArray arr, RuntimeBase returned) for (RuntimeScalar elem : arr.elements) { if (returned == null) RuntimeScalar.releaseIoOwner(elem); else RuntimeScalar.releaseIoOwnerPreservingReturned(elem, returned); + if (elem != null) elem.releaseContainerOwner(arr); } } else if (arr.ownedAliasElements != null) { for (RuntimeScalar elem : arr.ownedAliasElements) { if (returned == null) RuntimeScalar.releaseIoOwner(elem); else RuntimeScalar.releaseIoOwnerPreservingReturned(elem, returned); + if (elem != null) elem.releaseContainerOwner(arr); } } // Skip container walks only when there are NO blessed objects AND NO @@ -934,22 +868,23 @@ private static void releaseTailCallArgElement(RuntimeScalar scalar) { if (--base.refCount > 0) return; - if (base.localBindingExists || reachableTailCallArgReferent(base)) { + // Counted owner slots are authoritative even when the compatibility + // refcount transiently undercounts them during a tail-call handoff. + // Restore from those identities before allowing final release. + if (base.restoreCountedOwnerSlotRefCount()) return; + + if (base.localBindingExists) { base.refCount = 1; return; } - + if (base.hasUncountedPerlOwnerSlotIdentity()) { + base.deferFinalReleaseToUncountedOwnerSlots(); + return; + } base.refCount = Integer.MIN_VALUE; DestroyDispatch.callDestroy(base); } - private static boolean reachableTailCallArgReferent(RuntimeBase base) { - return WeakRefRegistry.hasWeakRefsTo(base) - && (ReachabilityWalker.isReachableFromRoots(base) - || ReachabilityWalker.isReachableFromLiveScalarRegistry(base) - || ReachabilityWalker.isReachableFromLiveCodeCaptures(base)); - } - /** * Iteratively process a scalar value: if it holds a reference to a * tracked blessed object and owns a refCount, defer a decrement. @@ -959,6 +894,7 @@ private static boolean reachableTailCallArgReferent(RuntimeBase base) { */ private static void deferDecrementRecursive(RuntimeScalar scalar) { if (scalar == null || (scalar.type & RuntimeScalarType.REFERENCE_BIT) == 0) return; + if (WeakRefRegistry.isweak(scalar)) return; if (!(scalar.value instanceof RuntimeBase topBase)) return; // Fast path for leaf references (the overwhelmingly common case): @@ -1003,6 +939,7 @@ private static void deferDecrementRecursive(RuntimeScalar scalar) { while (!work.isEmpty()) { RuntimeScalar s = work.poll(); if (s == null || (s.type & RuntimeScalarType.REFERENCE_BIT) == 0) continue; + if (WeakRefRegistry.isweak(s)) continue; if (!(s.value instanceof RuntimeBase base)) continue; if (!visited.add(base)) continue; // already visited — cycle @@ -1014,6 +951,8 @@ private static void deferDecrementRecursive(RuntimeScalar scalar) { base.traceRefCount(0, "MortalList.deferDecrementRecursive (blessed, queued)"); base.releaseOwner(s, "deferDecrementRecursive blessed"); } + RuntimeBase.PendingOwnerRelease ownerRelease = base.queueOwnerRelease( + s, "MortalList.deferDecrementRecursive blessed"); base.releaseActiveOwner(s); // refCount is the authoritative ownership count here. // activeOwners is diagnostic and can retain a stale @@ -1021,7 +960,7 @@ private static void deferDecrementRecursive(RuntimeScalar scalar) { releasingLastOwner = base.refCount == 1; LifecycleRuntimeState state = state(); markBoundaryWork(state); - queueDeferredBase(state, base, null, null, s); + queueDeferredBase(state, base, ownerRelease, null, s); } else if (base.refCount == 0) { if (base.refCountTrace) { base.traceRefCount(+1, "MortalList.deferDecrementRecursive (blessed never-stored bump+queue)"); @@ -1061,10 +1000,12 @@ private static void deferDecrementRecursive(RuntimeScalar scalar) { if (base.refCountTrace) { base.traceRefCount(0, "MortalList.deferDecrementRecursive (unblessed container, queued)"); } + RuntimeBase.PendingOwnerRelease ownerRelease = base.queueOwnerRelease( + s, "MortalList.deferDecrementRecursive unblessed"); base.releaseActiveOwner(s); LifecycleRuntimeState state = state(); markBoundaryWork(state); - queueDeferredBase(state, base, null, null, s); + queueDeferredBase(state, base, ownerRelease, null, s); if (!WeakRefRegistry.weakRefsExist() && base.refCount > 1) { continue; } @@ -1092,6 +1033,11 @@ && isReachableFromExternalRootCached(base)))) { // release the original owner's children. continue; } + // A registered lexical aggregate owns its element slots even + // when another scalar reference to the aggregate is released. + // Decrement this scalar's aggregate edge above, then leave the + // elements for the lexical aggregate's own scope cleanup. + if (base.localBindingExists && MyVarCleanupStack.isRegistered(base)) continue; // Walk into unblessed containers to find nested blessed refs if (base instanceof RuntimeArray arr) { for (RuntimeScalar elem : arr.elements) { @@ -1174,13 +1120,13 @@ public static void mortalizeForVoidDiscard(RuntimeList result) { if (elem instanceof RuntimeScalar scalar && (scalar.type & RuntimeScalarType.REFERENCE_BIT) != 0 && scalar.value instanceof RuntimeBase base) { + if (scalar.hasStorageOwnerForVoidDiscard()) continue; boolean unblessedDiscardableContainer = base.blessId == 0 && (base instanceof RuntimeHash || base instanceof RuntimeArray || base instanceof RuntimeCode); boolean reachableCodeRef = base instanceof RuntimeCode - && scalar.isStoredInRegisteredContainerOwner() - && ReachabilityWalker.isScalarReachable(scalar); + && scalar.isStoredInRegisteredContainerOwner(); if (scalar.refCountOwned && base.refCount > 0 && unblessedDiscardableContainer && !reachableCodeRef) { scalar.refCountOwned = false; @@ -1188,10 +1134,12 @@ public static void mortalizeForVoidDiscard(RuntimeList result) { base.traceRefCount(0, "MortalList.mortalizeForVoidDiscard (queued owned container discard)"); base.releaseOwner(scalar, "mortalizeForVoidDiscard container"); } + RuntimeBase.PendingOwnerRelease ownerRelease = base.queueOwnerRelease( + scalar, "MortalList.mortalizeForVoidDiscard"); base.releaseActiveOwner(scalar); LifecycleRuntimeState state = state(); markBoundaryWork(state); - queueDeferredBase(state, base, null); + queueDeferredBase(state, base, ownerRelease, null, scalar); } else if (base.refCount == 0 && (base.blessId != 0 || base instanceof RuntimeHash @@ -1213,20 +1161,20 @@ public static void mortalizeForVoidDiscard(RuntimeList result) { // Each mark records the pending list size at scope entry, so that // popAndFlush() only processes entries added within that scope. private static void processDeferredEntriesFrom( - int pendingStartIdx, int tiedReleaseStartIdx, int ioReleaseStartIdx) { + int pendingStartIdx, int tiedReleaseStartIdx, int ioReleaseStartIdx, + int untrackedReturnOwnerStartIdx) { LifecycleRuntimeState state = state(); - boolean previousCaptureSweepDeferral = - state.deferCaptureReachabilityToBoundarySweep; - state.deferCaptureReachabilityToBoundarySweep = - previousCaptureSweepDeferral || canDeferCapturedTargetCheckToBoundarySweep(state); state.reachabilityDrainDepth++; try { int pendingIdx = pendingStartIdx; int tiedReleaseIdx = tiedReleaseStartIdx; int ioReleaseIdx = ioReleaseStartIdx; + int untrackedReturnOwnerIdx = untrackedReturnOwnerStartIdx; while (pendingIdx < state.pending.size() || tiedReleaseIdx < state.pendingTiedReleases.size() - || ioReleaseIdx < state.pendingIoReleases.size()) { + || ioReleaseIdx < state.pendingIoReleases.size() + || untrackedReturnOwnerIdx + < state.pendingUntrackedReturnOwnerReleases.size()) { while (tiedReleaseIdx < state.pendingTiedReleases.size()) { // Releasing a tie handler changes the graph represented by // the per-drain tied-reachability snapshot. @@ -1234,19 +1182,19 @@ private static void processDeferredEntriesFrom( state.pendingTiedReleases.get(tiedReleaseIdx++).releaseTiedObject(); } while (pendingIdx < state.pending.size()) { + int currentIndex = pendingIdx++; RuntimeBase.PendingOwnerRelease ownerRelease = - state.pendingOwnerReleases.get(pendingIdx); - String transientOwnerKind = state.pendingTransientOwnerKinds.get(pendingIdx); - RuntimeBase base = state.pending.get(pendingIdx); - java.lang.ref.WeakReference ownerRef = - state.pendingOwnerScalars.get(pendingIdx++); - RuntimeScalar ownerScalar = ownerRef == null ? null : ownerRef.get(); - if (base.blessId != 0 - && WeakRefRegistry.hasWeakRefsTo(base) - && RuntimeCode.hasActiveArgumentOwner(ownerScalar)) { + state.pendingOwnerReleases.get(currentIndex); + String transientOwnerKind = + state.pendingTransientOwnerKinds.get(currentIndex); + RuntimeBase base = state.pending.get(currentIndex); + boolean argumentAliasOwnsBase = + base.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS) != 0 + && base.countedOwnerSlotCount() == 0; + if (argumentAliasOwnsBase) { state.deferredArgumentOwnerReleases.add( new DeferredArgumentOwnerRelease(base, ownerRelease, - transientOwnerKind, ownerScalar)); + transientOwnerKind)); } else { processDeferredBase(base, false, ownerRelease, transientOwnerKind); } @@ -1254,10 +1202,15 @@ private static void processDeferredEntriesFrom( while (ioReleaseIdx < state.pendingIoReleases.size()) { RuntimeScalar.releaseIoOwner(state.pendingIoReleases.get(ioReleaseIdx++)); } + while (untrackedReturnOwnerIdx + < state.pendingUntrackedReturnOwnerReleases.size()) { + RuntimeScalar scalar = + state.pendingUntrackedReturnOwnerReleases.get(untrackedReturnOwnerIdx++); + if (scalar != null) scalar.releaseMortalReturnValueOwner(); + } } } finally { state.reachabilityDrainDepth--; - state.deferCaptureReachabilityToBoundarySweep = previousCaptureSweepDeferral; } drainDeferredArgumentOwnerReleases(); } @@ -1269,8 +1222,10 @@ static void drainDeferredArgumentOwnerReleases() { new ArrayList<>(state.deferredArgumentOwnerReleases); state.deferredArgumentOwnerReleases.clear(); for (DeferredArgumentOwnerRelease deferred : pending) { - RuntimeScalar ownerScalar = deferred.ownerScalar.get(); - if (RuntimeCode.hasActiveArgumentOwner(ownerScalar)) { + if (deferred.base.countedOwnerSlotCount() != 0) { + processDeferredBase(deferred.base, false, deferred.ownerRelease, + deferred.transientOwnerKind); + } else if (deferred.base.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS) != 0) { state.deferredArgumentOwnerReleases.add(deferred); } else { processDeferredBase(deferred.base, false, deferred.ownerRelease, @@ -1279,22 +1234,6 @@ static void drainDeferredArgumentOwnerReleases() { } } - private static void finalizeDeferredCaptureSweepCandidates( - LifecycleRuntimeState state) { - if (state.deferredCaptureSweepCandidates.isEmpty()) return; - if (state.boundaryWeakSweepGeneration > state.deferredCaptureSweepGeneration) { - state.deferredCaptureSweepCandidates.clear(); - state.deferredCaptureSweepGeneration = state.boundaryWeakSweepGeneration; - } - } - - private static boolean canDeferCapturedTargetCheckToBoundarySweep( - LifecycleRuntimeState state) { - return !AUTO_GC_DISABLED - && !state.inAutoSweep - && !ModuleInitGuard.inModuleInit(); - } - /** * Process all pending decrements. Called at statement boundaries. * Equivalent to Perl 5's FREETMPS. @@ -1339,164 +1278,45 @@ public static boolean suppressFlush(boolean suppress) { // (100ms, 500ms) fire too frequently — 52leaks.t creates thousands // of weaken'd refs and each sweep's System.gc() + weak-ref cascade // can run for tens of seconds. 5s gives the walker time to amortize. - // - // Trade-off: tests that rely on deterministic DESTROY after `undef` - // of a blessed ref (e.g. t/storage/error.t test 49) need explicit - // Internals::jperl_gc() to fire the walker within their short - // wall-clock. - private static final long AUTO_SWEEP_MIN_INTERVAL_NS = 5_000_000_000L; - private static final boolean AUTO_GC_DISABLED = - System.getenv("JPERL_NO_AUTO_GC") != null; - private static final boolean AUTO_GC_DEBUG = - System.getenv("JPERL_GC_DEBUG") != null; - private static final boolean WEAK_SWEEP_REQUEST_DEBUG = - System.getenv("JPERL_WEAK_SWEEP_REQUEST_DEBUG") != null; - // Phase D-W6.20 (debug knob): force the auto-sweep on EVERY flush() - // call, bypassing the 5-s throttle and the `weakRefsExist` gate. - // Used to reproduce timing-dependent walker bugs (e.g. the - // ScalarRefRegistry-vs-forceGc race that surfaces as DBIC's - // "detached result source" mid-test crash). Off by default; when on, - // every Perl statement boundary triggers a full sweepWeakRefs walk — - // very slow, only for diagnostics. - private static final boolean FORCE_SWEEP_EVERY_FLUSH = - System.getenv("JPERL_FORCE_SWEEP_EVERY_FLUSH") != null; - - // Objects explicitly released by undef while JVM statement temporaries may - // still make them appear reachable. Recheck only these referents at the - // next Perl statement boundary; a full nested-call sweep can invalidate - // unrelated weak callback graphs whose caller-owned return values have not - // reached a walker-visible lexical yet. + /** Compatibility hook; weak-reference cleanup is driven by owner release. */ public static void requestImmediateWeakSweep() { - LifecycleRuntimeState state = state(); - markBoundaryWork(state); - state.immediateWeakSweepRequested = true; - if (WEAK_SWEEP_REQUEST_DEBUG) { - StackTraceElement[] stack = Thread.currentThread().getStackTrace(); - StackTraceElement caller = stack.length > 2 ? stack[2] : null; - System.err.println("[WEAK_SWEEP_REQUEST] caller=" + caller); - } } - public static void requestTargetedWeakSweep(RuntimeBase referent) { - if (referent != null) { - LifecycleRuntimeState state = state(); - markBoundaryWork(state); - state.targetedWeakSweepReferents.add(referent); - } + /** Compatibility hook; weak-reference cleanup is driven by owner release. */ + static void requestCorrectiveWeakSweep() { } - /** - * Finish targeted releases queued by an explicit aggregate discard after - * the assignment's temporary scalar roots have been removed. Unlike the - * global quiet sweep, this walk is safe at nested statement boundaries - * because it considers only referents reached from the discarded graph. - */ - public static void flushReleasedWeakSweepsIfSafe() { - LifecycleRuntimeState state = state(); - if (state.targetedWeakSweepReferents.isEmpty()) return; - Set releasedTargets = Collections.newSetFromMap(new IdentityHashMap<>()); - releasedTargets.addAll(state.targetedWeakSweepReferents); - state.targetedWeakSweepReferents.clear(); - ReachabilityWalker.ReleasedWeakSweepResult releasedSweepResult = - ReachabilityWalker.sweepReleasedWeakReferents(releasedTargets); - - // A complete quiet sweep may already be required at this safe boundary. - // Reuse the targeted pass's root set for that registry-wide cleanup, - // avoiding a second traversal when releasing these referents caused no - // DESTROY cascade that would have changed the graph. - if (releasedSweepResult.live() == null - || !state.immediateWeakSweepRequested - || AUTO_GC_DISABLED - || FORCE_SWEEP_EVERY_FLUSH - || state.inAutoSweep - || ModuleInitGuard.inModuleInit() - || RuntimeCode.argsStackDepth() > 1 - || !state.temporaryRoots.isEmpty()) { - return; - } - state.immediateWeakSweepRequested = false; - state.lastAutoSweepNanos = System.nanoTime(); - state.inAutoSweep = true; - try { - ReachabilityWalker.sweepWeakRefs( - true, false, releasedTargets, releasedSweepResult); - state.boundaryWeakSweepGeneration++; - finalizeDeferredCaptureSweepCandidates(state); - } finally { - state.inAutoSweep = false; - } + /** Compatibility hook; weak-reference cleanup is driven by owner release. */ + public static void requestTargetedWeakSweep(RuntimeBase referent) { } - /** - * Register weakly tracked referents whose last strong edge may disappear - * with a container temporary. References to ordinary scalar referents - * cannot participate in the selective refCount scheme, so a discarded - * anonymous array/hash must explicitly ask the statement-boundary walker - * to reconsider them. This is the aggregate equivalent of assigning - * {@code undef} to a scalar that contains a WEAKLY_TRACKED reference. - */ + /** Compatibility hook retained for callers compiled against the old sweep API. */ public static boolean requestWeakSweepsForDestroyedContainer(RuntimeBase container) { - if (!(container instanceof RuntimeArray) && !(container instanceof RuntimeHash)) return false; - - ArrayDeque work = new ArrayDeque<>(); - Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); - work.add(container); - boolean requested = WeakRefRegistry.hasWeakRefsTo(container); - if (requested) requestTargetedWeakSweep(container); - - while (!work.isEmpty()) { - RuntimeBase current = work.removeFirst(); - if (!visited.add(current)) continue; - - // A stash is a symbol-table view, not an owned aggregate. Walking - // its dynamic HashSpecialVariable map enumerates package globals - // on every scope exit (Catalyst creates many short-lived - // containers), repeatedly traversing the whole program graph. - // Its values are roots already handled by the normal weak sweep. - if (current instanceof RuntimeStash) continue; - - Iterable values; - if (current instanceof RuntimeArray array) { - values = array.elements; - } else if (current instanceof RuntimeHash hash) { - values = hash.elements.values(); - } else { - continue; - } - - for (RuntimeScalar value : values) { - if (value == null - || (value.type & RuntimeScalarType.REFERENCE_BIT) == 0 - || !(value.value instanceof RuntimeBase referent)) { - continue; - } - if (WeakRefRegistry.hasWeakRefsTo(referent)) { - requestTargetedWeakSweep(referent); - requested = true; - } - if (referent instanceof RuntimeArray || referent instanceof RuntimeHash) { - work.add(referent); - } - } - } - return requested; + return false; } static void finalizeClearedAggregateOwnerAfterScopeExit(RuntimeBase base) { if (!isActive() || base == null - || base.refCount != 0 || !base.clearedOwnedAggregateElement || !WeakRefRegistry.hasWeakRefsTo(base)) { return; } - // This check runs at individual lexical scope exits, often many times - // while a package is loading. Keep package-root traversal cached - // across those exits; the live lexical snapshot is independently - // invalidated as each lexical leaves scope. - if (isReachableFromExternalRootCached(base)) { + // Scope cleanup retires the exact lexical owner before reaching this + // point. Package, container, and capture edges have their own owner + // slots; temporary roots are checked separately because they are + // evaluation scaffolding rather than Perl ownership. Prefer these + // identities, retaining the cached graph proof only for ownerless + // compatibility cases that have not yet migrated. + if (base.hasPerlStrongOwnerIdentity() || temporaryRootDirectlyReferences(base)) { return; } + // A deferred count release has already retired its live owner slot, + // but the compatibility count is not decremented until the mortal + // queue drains. Let that exact release decide finalization instead of + // racing it with scope-exit cleanup. + if (base.hasPendingOwnerRelease()) return; + if (isReachableFromExternalRootCached(base)) return; base.refCount = Integer.MIN_VALUE; DestroyDispatch.callDestroy(base); } @@ -1745,6 +1565,13 @@ private static boolean isReachableThroughTiedHashCached(RuntimeBase base) { private static void processDeferredBase(RuntimeBase base, boolean clearWeakRefsForLocalBinding, RuntimeBase.PendingOwnerRelease ownerRelease, String transientOwnerKind) { + if (base.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS) != 0 + && base.countedOwnerSlotCount() == 0) { + state().deferredArgumentOwnerReleases.add( + new DeferredArgumentOwnerRelease(base, ownerRelease, + transientOwnerKind)); + return; + } ReachabilityQueryStats queryStats = activeReachabilityQueryStats(); if (queryStats != null) queryStats.deferredBasesProcessed++; base.completeQueuedOwnerRelease(ownerRelease, "MortalList.processDeferredBase"); @@ -1755,7 +1582,16 @@ private static void processDeferredBase(RuntimeBase base, boolean clearWeakRefsF base.traceRefCount(-1, "MortalList.flush (deferred decrement)"); } if (base.refCount > 0 && --base.refCount == 0) { - if (base.hasSemanticCaptureOwner()) { + boolean aggregateReferent = base instanceof RuntimeArray || base instanceof RuntimeHash; + // Aggregate owner slots have their own deferred-release rule below. + // Refresh scalar-cell owners here for other referents before the + // final counted release checks whether any owner slot remains. + boolean hasUntrackedReferenceOwner = !aggregateReferent + && base.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE) != 0 + && base.hasUntrackedReferenceOwnerSlotIdentity(); + if (base instanceof RuntimeCode code && code.hasActiveCodeOwner()) { + code.deferFinalReleaseToActiveCode(); + } else if (base.hasSemanticCaptureOwner()) { // Captured aggregate pads use native owner slots. Keep the // legacy count at zero while one is active; its final release // will perform the lifecycle transition. Scalar captures still @@ -1789,6 +1625,19 @@ private static void processDeferredBase(RuntimeBase base, boolean clearWeakRefsF if (clearWeakRefsForLocalBinding) { WeakRefRegistry.clearWeakRefsTo(base); } + } else if (isFinalCountedReferentRelease(base, ownerRelease)) { + // This decrement retired an identified scalar/element edge. + // Every other migrated edge is already absent from the ledger; + // a cached root snapshot must not invent a replacement owner. + base.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroyFromCountedOwner(base); + } else if (base.blessId != 0 && base.restoreCountedOwnerSlotRefCount()) { + // For blessed referents, remaining counted slots prove that + // the transient zero was an undercount. Unblessed referents + // use stricter current-storage checks below. + } else if (base.restoreVerifiedCurrentCountedOwnerSlotRefCount()) { + // Verified current scalar cells and array/hash elements jointly + // restore the surviving owner count without a root walk. } else if (base instanceof RuntimeCode code && hasWeakRefs && (RuntimeCode.isActiveCode(code) @@ -1800,24 +1649,26 @@ private static void processDeferredBase(RuntimeBase base, boolean clearWeakRefsF // nested dispatch call undefer_sub(undef). base.refCount = 1; } else if (base instanceof RuntimeScalarReadOnly - && hasWeakRefs && RuntimeCode.isInstalledPadConstant(base)) { - // A readonly scalar referenced through \("literal") belongs to - // its defining subroutine's pad. Constructor/call temporaries - // are selectively counted, but that pad ownership is not. - // Preserve the weak ref while the sub remains installed; - // RuntimeCode.clearPadConstantWeakRefs() clears it when the - // sub's glob is replaced, matching Perl optree reaping. - base.refCount = 1; + // A readonly scalar referenced through \("literal") has an + // explicit owner slot from each live CV optree that contains it. + // Defer lifecycle cleanup to those slots instead of fabricating + // a legacy count that has no corresponding Perl reference. + base.deferFinalReleaseToUncountedOwnerSlots(); + } else if (base.hasNonPackageAggregateUncountedOwnerSlotIdentity() + || (aggregateReferent + ? base.hasUntrackedReferenceOwnerSlotIdentity( + RuntimeScalarType.REFERENCE) + : hasUntrackedReferenceOwner)) { + // Counted owner recovery is complete above. A surviving + // aggregate or untracked scalar slot is enough to defer cleanup + // until its release, even when no weak observer currently exists. + base.deferFinalReleaseToUncountedOwnerSlots(); } else if (hasWeakRefs && restoreReachableOwnerCount(base)) { // A counted owner that is still reachable through a real // Perl root is authoritative, including for classes with - // DESTROY. Pure-Perl tied containers expose this shape: - // Test::Spec stores a context in Tie::IxHash and weakens the - // child's parent link. Method/argument temporaries can make - // the selective count dip to zero, but the tied handler's - // array slot remains a strong owner. Destroying here clears - // that weak parent and corrupts the context tree. + // DESTROY. Unmigrated slots still need this compatibility + // fallback until their owner identities are complete. } else if (base.blessId == 0 && (base instanceof RuntimeArray || base instanceof RuntimeHash) && isUnblessedAggregateReachableDuringDrain(base)) { @@ -1853,22 +1704,6 @@ && isReachableThroughTiedHashCached(base)) { // target. Keep it alive without requesting a registry-wide // sweep; unknown targets still use the complete-boundary path. base.refCount = 1; - } else if (base.blessId == 0 - && hasWeakRefs - && canDeferCapturedTargetCheckToBoundarySweep(state())) { - // Carry released unblessed targets to the next safe complete - // sweep. This avoids scanning live and installed CODE captures - // repeatedly after an inconclusive query while keeping their - // weak observers valid until the sweep decides reachability. - if (state().deferredCaptureSweepCandidates.isEmpty()) { - state().deferredCaptureSweepGeneration = - state().boundaryWeakSweepGeneration; - } - // Keep the target alive until the next safe complete sweep - // decides whether a live CODE capture still owns it. - base.refCount = 1; - state().deferredCaptureSweepCandidates.add(base); - requestImmediateWeakSweep(); } else if (base.blessId == 0 && hasWeakRefs && RuntimeCode.argsStackDepth() > 1) { @@ -1913,18 +1748,6 @@ && canDeferCapturedTargetCheckToBoundarySweep(state())) { // protect live objects. Classes with DESTROY keep the stricter // path. base.refCount = 1; - } else if (base.blessId != 0 - && hasWeakRefs - && !blessedClassHasDestroy(base) - && RuntimeCode.argsStackDepth() > 1 - && !base.clearedOwnedAggregateElement) { - // Nested calls can retain expression temporaries which are not - // selective refcount owners. Leave the count at zero and let - // the next outer statement boundary decide from Perl-visible - // reachability. Retaining a synthetic count here leaks weak - // observers after a method-local alias releases the final real - // owner (Algorithm::SlidingWindow). - requestTargetedWeakSweep(base); } else if (base.blessId != 0 && base.storedInPackageGlobal && hasWeakRefs @@ -1934,10 +1757,35 @@ && isReachableCached(base)) { // If the walker can still reach the object from Perl-visible // roots, keep weak links to it intact. } else { + // Call depth is not ownership. If every counted and direct + // owner check above is empty, a weak referent is final here; + // leaving it for a later sweep would strand the weak scalars. base.refCount = Integer.MIN_VALUE; DestroyDispatch.callDestroy(base); } } + // A prior compatibility branch may preserve refCount at zero while a + // nested scope is unwinding. The exact owner release is still pending + // in this call, so recheck its ledger after that branch: once this was + // the final trusted slot and no other Perl binding remains, finish the + // release here instead of relying on a later reachability sweep. + if (base.refCount == 0 + && isFinalCountedReferentRelease(base, ownerRelease) + && !base.localBindingExists + && base.captureCount == 0 + && !base.hasPendingReturnOwner() + && !GlobalVariable.isPackageRootedAggregate(base) + && !(base instanceof RuntimeCode code + && (code.hasActiveCodeOwner() + || code.stashRefCount > 0 + || GlobalVariable.installedGlobalCodeRefsView().contains(code)))) { + base.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroyFromCountedOwner(base); + } + if (base instanceof RuntimeCode code) code.finishActiveCodeOwnerRelease(); + if (base instanceof RuntimeScalar scalar) { + PerlOwnerSlot.finishDeferredScalarCellRelease(scalar); + } } private static boolean blessedClassHasDestroy(RuntimeBase base) { @@ -1945,6 +1793,28 @@ private static boolean blessedClassHasDestroy(RuntimeBase base) { return className != null && DestroyDispatch.classHasDestroy(base.blessId, className); } + static boolean canTrustFinalCountedRelease(RuntimeBase base) { + boolean supportedReferent = base instanceof RuntimeArray || base instanceof RuntimeHash + || (base instanceof RuntimeScalar scalar + && !(scalar instanceof RuntimeScalarReadOnly) && scalar.captureCount == 0) + || (base instanceof RuntimeCode code + && (code.hasCountedCodeSlots || (!code.hadStashRef && code.stashRefCount <= 0)) + && code.activeEndBlockExecutions == 0 + && !SpecialBlock.hasPendingEndBlock(code)); + return supportedReferent && !base.hasOwnerSlots(); + } + + private static boolean isFinalCountedReferentRelease( + RuntimeBase base, RuntimeBase.PendingOwnerRelease release) { + if (!canTrustFinalCountedRelease(base) + || release == null || release.ownerSlotRelease == null) return false; + return switch (release.ownerSlotRelease.kind()) { + case SCALAR_CELL, CODE_SLOT, ARRAY_ELEMENT, HASH_ELEMENT, ARGUMENT_VALUE, RETURN_VALUE, + TIE_HANDLER -> true; + default -> false; + }; + } + private static boolean restoreReachableOwnerCount(RuntimeBase base) { int ownerCount = base.reachableOwnerCount(); if (ownerCount == 0) return false; @@ -1956,12 +1826,18 @@ public static void flush() { LifecycleRuntimeState state = state(); if (!state.mortalActive) return; if (state.flushing) return; + if (state.sweepPendingAfterOuterDestroy) { + // Preserve the old diagnostic counters for explicit post-DESTROY + // requests. This snapshots CODE captures only; it never clears + // weak refs, dispatches DESTROY, or changes owner state. + state.sweepPendingAfterOuterDestroy = false; + ReachabilityWalker.inspectWeakBackrefCapturesForDiagnostics(); + } boolean topLevel = !state.hasMortalMarks(); if (state.pending.isEmpty() && state.pendingTiedReleases.isEmpty() - && state.pendingIoReleases.isEmpty()) { + && state.pendingIoReleases.isEmpty() + && state.pendingUntrackedReturnOwnerReleases.isEmpty()) { processReadyDeferredCaptures(state); - maybeAutoSweep(state); - finalizeDeferredCaptureSweepCandidates(state); refreshBoundaryWork(state); return; } @@ -1969,13 +1845,15 @@ public static void flush() { state.flushing = true; if (topLevel) state.topLevelFlushDepth++; try { - processDeferredEntriesFrom(0, 0, 0); + processDeferredEntriesFrom(0, 0, 0, 0); state.pending.clear(); state.pendingOwnerReleases.clear(); state.pendingOwnerScalars.clear(); + state.pendingArgumentFrames.clear(); state.pendingTransientOwnerKinds.clear(); state.pendingTiedReleases.clear(); state.pendingIoReleases.clear(); + state.pendingUntrackedReturnOwnerReleases.clear(); state.markDepth = 0; // All entries drained; marks are meaningless now } finally { if (topLevel) state.topLevelFlushDepth--; @@ -1983,99 +1861,6 @@ public static void flush() { invalidateDrainReachabilityCaches(); } processReadyDeferredCaptures(state); - maybeAutoSweep(state); - finalizeDeferredCaptureSweepCandidates(state); - refreshBoundaryWork(state); - } - - private static void maybeAutoSweep() { - maybeAutoSweep(state()); - } - - private static void maybeAutoSweep(LifecycleRuntimeState state) { - maybeAutoSweep(state, Collections.emptySet(), null); - } - - private static void maybeAutoSweep( - LifecycleRuntimeState state, - Set releasedTargets, - ReachabilityWalker.ReleasedWeakSweepResult releasedSweepResult) { - if (state.weakSweepSuppressionDepth > 0) return; - if (AUTO_GC_DISABLED) return; - if (state.inAutoSweep) return; - boolean immediateSweep = state.immediateWeakSweepRequested; - // FORCE_SWEEP_EVERY_FLUSH bypasses the weakRefsExist gate AND the - // 5-s throttle so reproducers can deterministically trigger the - // walker at every statement boundary. - if (!FORCE_SWEEP_EVERY_FLUSH && !state.weakRefsExist && !immediateSweep) return; - // Phase B2a: skip while require/use/BEGIN/eval-STRING is running. - // Those paths depend on weak-refed intermediate state staying - // defined until the init completes. - if (ModuleInitGuard.inModuleInit()) return; - // Sweeps must stay out of nested calls. Active call frames can hold - // temporaries and closure metadata through JVM locals that are not - // complete Perl-visible walker roots yet. - if (RuntimeCode.argsStackDepth() > 1) return; - if (!state.temporaryRoots.isEmpty()) return; - if (!FORCE_SWEEP_EVERY_FLUSH && !immediateSweep) { - long now = System.nanoTime(); - if (now - state.lastAutoSweepNanos < AUTO_SWEEP_MIN_INTERVAL_NS) return; - state.lastAutoSweepNanos = now; - } - state.inAutoSweep = true; - try { - if (immediateSweep) { - state.immediateWeakSweepRequested = false; - state.lastAutoSweepNanos = System.nanoTime(); - } - // Quiet auto-sweeps run from normal statement-boundary checks. - // Do not force HotSpot GC here: current live-lexical tracking is - // driven by MyVarCleanupStack, and forcing JVM GC at this cadence - // dominates DBIC-scale runtimes. Keep the old forced behavior only - // for the diagnostic "sweep every flush" mode. - int cleared = ReachabilityWalker.sweepWeakRefs( - true, FORCE_SWEEP_EVERY_FLUSH, releasedTargets, releasedSweepResult); - state.boundaryWeakSweepGeneration++; - if (AUTO_GC_DEBUG) { - System.err.println("DBG auto-sweep reason=" - + (immediateSweep ? "immediate" : "cadence") - + " cleared=" + cleared); - } - } finally { - state.inAutoSweep = false; - } - } - - private static void maybeAutoSweepIfRequested(LifecycleRuntimeState state) { - if (state.immediateWeakSweepRequested) { - maybeAutoSweep(state); - } - } - - private static void maybeAutoSweepAtStatementBoundary( - LifecycleRuntimeState state, boolean topLevel) { - if (state.weakSweepSuppressionDepth > 0) return; - Set releasedTargets = Collections.emptySet(); - ReachabilityWalker.ReleasedWeakSweepResult releasedSweepResult = null; - if (!state.targetedWeakSweepReferents.isEmpty() && state.temporaryRoots.isEmpty()) { - releasedTargets = Collections.newSetFromMap(new IdentityHashMap<>()); - releasedTargets.addAll(state.targetedWeakSweepReferents); - state.targetedWeakSweepReferents.clear(); - releasedSweepResult = - ReachabilityWalker.sweepReleasedWeakReferents(releasedTargets); - } - if (topLevel) { - maybeAutoSweep(state, releasedTargets, releasedSweepResult); - finalizeDeferredCaptureSweepCandidates(state); - } - } - - private static void maybeSweepStatementBoundaryDestroyables() { - LifecycleRuntimeState state = state(); - if (!ModuleInitGuard.inModuleInit() - && DestroyDispatch.hasStatementBoundaryDestroyableObjects()) { - ReachabilityWalker.sweepStatementBoundaryDestroyableObjects(); - } refreshBoundaryWork(state); } @@ -2126,6 +1911,7 @@ public static void drainPendingSince(int startIdx) { state.pending.remove(state.pending.size() - 1); state.pendingOwnerReleases.remove(state.pendingOwnerReleases.size() - 1); state.pendingOwnerScalars.remove(state.pendingOwnerScalars.size() - 1); + state.pendingArgumentFrames.remove(state.pendingArgumentFrames.size() - 1); state.pendingTransientOwnerKinds.remove(state.pendingTransientOwnerKinds.size() - 1); } } @@ -2156,7 +1942,8 @@ public static void drainPendingTailCallArgs( RuntimeScalar owner = ownerRef == null ? null : ownerRef.get(); RuntimeBase pending = state.pending.get(i); boolean directOwner = owner != null && owners.contains(owner); - boolean frameCopy = owner != null && owner.copiedFromArgumentFrame(argumentFrame); + boolean frameOwner = argumentFrame != null + && state.pendingArgumentFrames.get(i) == argumentFrame; boolean abandonedBirthTemporary = owner == null && "bless mortal temporary".equals(state.pendingTransientOwnerKinds.get(i)) && args.elements.stream().anyMatch(argument -> argument != null @@ -2169,7 +1956,7 @@ public static void drainPendingTailCallArgs( if (abandonedBirthTemporary && directlyReferences(returnedResult, pending)) { continue; } - if ((!directOwner && !frameCopy && !abandonedBirthTemporary) + if ((!directOwner && !frameOwner && !abandonedBirthTemporary) || (owner == null && !abandonedBirthTemporary)) continue; RuntimeBase.PendingOwnerRelease ownerRelease = state.pendingOwnerReleases.get(i); @@ -2177,6 +1964,7 @@ public static void drainPendingTailCallArgs( state.pending.remove(i); state.pendingOwnerReleases.remove(i); state.pendingOwnerScalars.remove(i); + state.pendingArgumentFrames.remove(i); state.pendingTransientOwnerKinds.remove(i); processDeferredBase(pending, true, ownerRelease, transientOwnerKind); } @@ -2205,7 +1993,8 @@ public static void pushMark() { static void pushMark(LifecycleRuntimeState state) { if (!state.mortalActive) return; state.pushMortalMark(state.pending.size(), state.pendingTiedReleases.size(), - state.pendingIoReleases.size()); + state.pendingIoReleases.size(), + state.pendingUntrackedReturnOwnerReleases.size()); } /** @@ -2242,10 +2031,9 @@ public static void flushAboveMark() { if (state.flushing) return; boolean topLevel = !state.hasMortalMarks(); if (state.pending.isEmpty() && state.pendingTiedReleases.isEmpty() - && state.pendingIoReleases.isEmpty()) { + && state.pendingIoReleases.isEmpty() + && state.pendingUntrackedReturnOwnerReleases.isEmpty()) { processReadyDeferredCaptures(state); - processUnreachableScopeExitCaptureCandidates(state); - maybeAutoSweepAtStatementBoundary(state, topLevel); refreshBoundaryWork(state); return; } @@ -2254,11 +2042,12 @@ public static void flushAboveMark() { ? state.tiedReleaseMarks[state.markDepth - 1] : 0; int ioMark = state.hasMortalMarks() ? state.ioReleaseMarks[state.markDepth - 1] : 0; + int untrackedReturnOwnerMark = state.hasMortalMarks() + ? state.untrackedReturnOwnerMarks[state.markDepth - 1] : 0; if (state.pending.size() <= mark && state.pendingTiedReleases.size() <= tiedMark - && state.pendingIoReleases.size() <= ioMark) { + && state.pendingIoReleases.size() <= ioMark + && state.pendingUntrackedReturnOwnerReleases.size() <= untrackedReturnOwnerMark) { processReadyDeferredCaptures(state); - processUnreachableScopeExitCaptureCandidates(state); - maybeAutoSweepAtStatementBoundary(state, topLevel); refreshBoundaryWork(state); return; } @@ -2266,12 +2055,13 @@ public static void flushAboveMark() { state.flushing = true; if (topLevel) state.topLevelFlushDepth++; try { - processDeferredEntriesFrom(mark, tiedMark, ioMark); + processDeferredEntriesFrom(mark, tiedMark, ioMark, untrackedReturnOwnerMark); // Remove only entries above the mark while (state.pending.size() > mark) { state.pending.removeLast(); state.pendingOwnerReleases.removeLast(); state.pendingOwnerScalars.removeLast(); + state.pendingArgumentFrames.removeLast(); state.pendingTransientOwnerKinds.removeLast(); } while (state.pendingTiedReleases.size() > tiedMark) { @@ -2280,14 +2070,15 @@ public static void flushAboveMark() { while (state.pendingIoReleases.size() > ioMark) { state.pendingIoReleases.removeLast(); } + while (state.pendingUntrackedReturnOwnerReleases.size() > untrackedReturnOwnerMark) { + state.pendingUntrackedReturnOwnerReleases.removeLast(); + } } finally { if (topLevel) state.topLevelFlushDepth--; state.flushing = false; invalidateDrainReachabilityCaches(); } processReadyDeferredCaptures(state); - processUnreachableScopeExitCaptureCandidates(state); - maybeAutoSweepAtStatementBoundary(state, topLevel); refreshBoundaryWork(state); } @@ -2314,26 +2105,26 @@ public static void popAndFlush() { int mark = state.marks[markIndex]; int tiedMark = state.tiedReleaseMarks[markIndex]; int ioMark = state.ioReleaseMarks[markIndex]; + int untrackedReturnOwnerMark = state.untrackedReturnOwnerMarks[markIndex]; if (state.pending.size() <= mark && state.pendingTiedReleases.size() <= tiedMark - && state.pendingIoReleases.size() <= ioMark) { + && state.pendingIoReleases.size() <= ioMark + && state.pendingUntrackedReturnOwnerReleases.size() <= untrackedReturnOwnerMark) { // Even if no mortal entries to process, check deferred captures // that may have become ready (captureCount reached 0) during // scope cleanup. processReadyDeferredCaptures(state); - processUnreachableScopeExitCaptureCandidates(state); - maybeSweepStatementBoundaryDestroyables(); - maybeAutoSweepIfRequested(state); - finalizeDeferredCaptureSweepCandidates(state); + DestroyDispatch.processRescuedObjects(); return; } invalidateDrainReachabilityCaches(); // Process entries from mark onwards (DESTROY may add new entries) - processDeferredEntriesFrom(mark, tiedMark, ioMark); + processDeferredEntriesFrom(mark, tiedMark, ioMark, untrackedReturnOwnerMark); // Remove only the entries we processed (keep entries before mark) while (state.pending.size() > mark) { state.pending.removeLast(); state.pendingOwnerReleases.removeLast(); state.pendingOwnerScalars.removeLast(); + state.pendingArgumentFrames.removeLast(); state.pendingTransientOwnerKinds.removeLast(); } while (state.pendingTiedReleases.size() > tiedMark) { @@ -2342,13 +2133,13 @@ public static void popAndFlush() { while (state.pendingIoReleases.size() > ioMark) { state.pendingIoReleases.removeLast(); } + while (state.pendingUntrackedReturnOwnerReleases.size() > untrackedReturnOwnerMark) { + state.pendingUntrackedReturnOwnerReleases.removeLast(); + } invalidateDrainReachabilityCaches(); // After processing mortals (which may have triggered releaseCaptures // via callDestroy), check if any deferred captures are now ready. processReadyDeferredCaptures(state); - processUnreachableScopeExitCaptureCandidates(state); - maybeSweepStatementBoundaryDestroyables(); - maybeAutoSweepIfRequested(state); - finalizeDeferredCaptureSweepCandidates(state); + DestroyDispatch.processRescuedObjects(); } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/MyVarCleanupStack.java b/src/main/java/org/perlonjava/runtime/runtimetypes/MyVarCleanupStack.java index 807c62df40..6615cbe71b 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/MyVarCleanupStack.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/MyVarCleanupStack.java @@ -126,6 +126,7 @@ public static void register(Object var) { liveCounts().merge(var, 1, Integer::sum); MortalList.invalidateLiveRootSnapshot(); } + if (var instanceof RuntimeScalar scalar) scalar.refreshUntrackedReferenceOwner(); } /** @@ -180,6 +181,7 @@ public static void unregister(Object var) { // callers that release a lexical slot directly. base.localBindingExists = false; } + releaseActiveAggregateOwnerAfterCleanup(var); noteVarLeftScope(var); return; } @@ -209,6 +211,12 @@ public static void replace(Object oldVar, Object newVar) { liveCounts.merge(newVar, 1, Integer::sum); MortalList.invalidateLiveRootSnapshot(); } + if (oldVar instanceof RuntimeScalar scalar) { + scalar.refreshUntrackedReferenceOwner(); + } + if (newVar instanceof RuntimeScalar scalar) { + scalar.refreshUntrackedReferenceOwner(); + } return; } } @@ -235,10 +243,10 @@ private static void decLiveCount(Object var) { } private static void noteVarLeftScope(Object var) { - if (var instanceof RuntimeBase base - && (base instanceof RuntimeArray || base instanceof RuntimeHash) - && WeakRefRegistry.weakRefsExist()) { - MortalList.requestWeakSweepsForDestroyedContainer(base); + if (var instanceof RuntimeScalar scalar) { + PerlOwnerSlot valueOwner = scalar.untrackedReferenceOwnerSlot(); + RuntimeBase referent = valueOwner == null ? null : valueOwner.referent(); + scalar.finalizeUntrackedValueAfterScopeExit(referent); } if (var instanceof RuntimeBase base && WeakRefRegistry.hasWeakRefsTo(base)) { requestWeakCleanupAfterScopeExit(base); @@ -256,7 +264,7 @@ private static void requestWeakCleanupAfterScopeExit(RuntimeBase base) { // them when a closure graph is being torn down. Recheck weak refs at // the scope boundary so the reachability walker can reconcile those // records with the remaining Perl-visible roots. - MortalList.requestImmediateWeakSweep(); + MortalList.requestCorrectiveWeakSweep(); } /** @@ -285,11 +293,20 @@ private static void unwindTo(int mark, ArrayList stack) { decRegisteredCount(var); decLiveCount(var); MortalList.evalExceptionScopeCleanup(var); + releaseActiveAggregateOwnerAfterCleanup(var); noteVarLeftScope(var); } } } + private static void releaseActiveAggregateOwnerAfterCleanup(Object var) { + if (!(var instanceof RuntimeBase base) + || (!(base instanceof RuntimeArray) && !(base instanceof RuntimeHash))) return; + if (RuntimeCode.releaseActiveLexicalAggregateOwner(base)) { + WeakRefRegistry.onActiveLexicalOwnerReleased(base); + } + } + /** * Called on normal exit in {@code RuntimeCode.apply()}. * Discards registrations without running cleanup (normal scope-exit diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlOwnerSlot.java b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlOwnerSlot.java index 5cf83c1ded..e3afa5a6b3 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlOwnerSlot.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlOwnerSlot.java @@ -1,11 +1,13 @@ package org.perlonjava.runtime.runtimetypes; +import java.lang.ref.WeakReference; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; /** - * Identity for one Perl strong-owner slot. + * Identity for one Perl strong-owner slot or explicitly tracked runtime + * compatibility hold. * *

The current runtime still uses selective {@link RuntimeBase#refCount} * transitions as its lifecycle bridge. This object records the slot identity @@ -17,17 +19,50 @@ public final class PerlOwnerSlot { private static final AtomicLong NEXT_ID = new AtomicLong(); public enum Kind { - CLOSURE_PAD + CLOSURE_PAD, + SCALAR_CAPTURE, + SCALAR_CELL, + CODE_SLOT, + /** Compiled CODE optree retaining one backslash-addressable pad constant. */ + CODE_PAD_CONSTANT, + /** Executable regex callback retaining its closure CODE object. */ + REGEX_CALLBACK, + ARRAY_ELEMENT, + HASH_ELEMENT, + ARGUMENT_VALUE, + ARGUMENT_ALIAS, + RETURN_VALUE, + ACTIVE_CODE, + TIE_HANDLER, + /** Package scalar table entry owning one scalar cell. */ + PACKAGE_SCALAR_SLOT, + /** Package array or hash table entry owning one aggregate. */ + PACKAGE_AGGREGATE_SLOT, + /** Active lexical frame or top-level pad entry owning an aggregate. */ + ACTIVE_LEXICAL, + /** Strong container edge whose referent is not on the legacy refcount bridge. */ + UNTRACKED_REFERENCE, + /** Temporary legacy count pin for a package-rooted weak aggregate. */ + WEAKLY_TRACKED_COMPATIBILITY_PIN, + /** Runtime-owned temporary value retained across a cleanup drain. */ + TEMPORARY_ROOT } - private final Kind kind; + private Kind kind; private final RuntimeScalar padCell; private final long identity = NEXT_ID.incrementAndGet(); private RuntimeBase referent; + /** Scalar cell that owns an untracked value edge; weak to avoid retaining its source. */ + private WeakReference untrackedScalarOwner; + /** Weak back-reference to the scalar implementing a counted cell owner. */ + private WeakReference countedScalarOwner; + /** CODE object that owns a closure-pad identity; weak to avoid a metadata cycle. */ + private WeakReference captureOwner; private PerlRuntime ownerRuntime; private LifecycleRuntimeState ownerRuntimeState; + private boolean registeredInOwnerLedger; private int legacyCaptureCount; - private volatile boolean nativeCaptureOwner; + private volatile boolean nativeOwner; private long nextReleaseSequence; /** Provenance token carried with one queued legacy decrement. */ @@ -36,11 +71,24 @@ public static final class PendingRelease { private final long sequence; private final Kind kind; private final AtomicBoolean completed = new AtomicBoolean(); + private final LifecycleRuntimeState retainedState; + private final RuntimeBase retainedReferent; private PendingRelease(long ownerSlotIdentity, long sequence, Kind kind) { + this(ownerSlotIdentity, sequence, kind, null); + } + + private PendingRelease(long ownerSlotIdentity, long sequence, Kind kind, + RuntimeBase referent) { this.ownerSlotIdentity = ownerSlotIdentity; this.sequence = sequence; this.kind = kind; + PerlRuntime runtime = PerlRuntime.currentOrNull(); + boolean transfer = kind == Kind.ARGUMENT_VALUE || kind == Kind.RETURN_VALUE + || kind == Kind.CODE_SLOT; + retainedReferent = transfer ? referent : null; + retainedState = retainedReferent == null || runtime == null + ? null : runtime.retainOwnerSlotReferent(retainedReferent); } public long ownerSlotIdentity() { @@ -60,7 +108,9 @@ public boolean isCompleted() { } void complete() { - completed.set(true); + if (completed.compareAndSet(false, true)) { + PerlRuntime.releaseOwnerSlotReferent(retainedState, retainedReferent); + } } } @@ -85,14 +135,47 @@ RuntimeScalar padCell() { return padCell; } + synchronized void associateUntrackedScalarOwner(RuntimeScalar scalar) { + if (kind != Kind.UNTRACKED_REFERENCE) { + throw new IllegalStateException("scalar owner association requires an untracked-reference slot"); + } + untrackedScalarOwner = scalar == null ? null : new WeakReference<>(scalar); + } + + synchronized RuntimeScalar untrackedScalarOwner() { + return untrackedScalarOwner == null ? null : untrackedScalarOwner.get(); + } + boolean isNativeCaptureOwner() { - return nativeCaptureOwner; + return kind == Kind.CLOSURE_PAD && nativeOwner; + } + + /** True while this active slot is counted directly by the semantic owner ledger. */ + public boolean isNativeOwner() { + return nativeOwner && referent != null; + } + + /** True if this slot uses native-ledger release semantics, including after release. */ + public boolean usesNativeOwnerLedger() { + return nativeOwner; } public synchronized RuntimeBase referent() { return referent; } + /** Scalar cell implementing this counted edge, when it has one. */ + public synchronized RuntimeScalar countedScalarOwner() { + return countedScalarOwner == null ? null : countedScalarOwner.get(); + } + + synchronized void associateCountedScalarOwner(RuntimeScalar scalar) { + if (!isCountedKind(kind)) { + throw new IllegalStateException("scalar owner association requires a counted slot"); + } + countedScalarOwner = scalar == null ? null : new WeakReference<>(scalar); + } + public synchronized boolean isActive() { return referent != null; } @@ -126,14 +209,317 @@ public synchronized void acquireLegacyCapture(RuntimeBase target) { /** Acquire a captured aggregate pad as an explicit owner slot. */ public synchronized void acquireCapture(RuntimeBase target) { + acquireCapture(target, null); + } + + /** Acquire an aggregate pad capture and record the CODE owner of this slot. */ + public synchronized void acquireCapture(RuntimeBase target, RuntimeCode owner) { + if (kind != Kind.CLOSURE_PAD) { + throw new IllegalStateException("capture acquisition requires a closure-pad slot"); + } + acquireNativeOwner(target); + captureOwner = owner == null ? null : new WeakReference<>(owner); + } + + /** Acquire this slot as one semantic Perl owner, without touching refCount. */ + public synchronized void acquireNativeOwner(RuntimeBase target) { Objects.requireNonNull(target, "target"); if (target.refCount == Integer.MIN_VALUE) { - throw new IllegalStateException("cannot capture a destroyed referent"); + throw new IllegalStateException("cannot own a destroyed referent"); + } + if (legacyCaptureCount != 0) { + throw new IllegalStateException("cannot migrate a slot with pending legacy releases"); } - nativeCaptureOwner = true; + nativeOwner = true; transferTo(target); } + /** Record one package scalar table entry as an identity-owned scalar cell. */ + synchronized void acquirePackageScalarIdentity(RuntimeScalar target) { + if (kind != Kind.PACKAGE_SCALAR_SLOT) { + throw new IllegalStateException("package scalar acquisition requires a package-slot identity"); + } + Objects.requireNonNull(target, "target"); + if (legacyCaptureCount != 0) { + throw new IllegalStateException("package scalar slot has pending legacy releases"); + } + transferWithoutRuntimeRoot(target, true); + } + + /** Record one compiled CODE optree's strong reference to a pad constant. */ + synchronized void acquireCodePadConstantIdentity(RuntimeBase target) { + if (kind != Kind.CODE_PAD_CONSTANT) { + throw new IllegalStateException("pad-constant acquisition requires a CODE-pad slot"); + } + Objects.requireNonNull(target, "target"); + if (target.refCount == Integer.MIN_VALUE) { + throw new IllegalStateException("cannot own a destroyed pad constant"); + } + if (legacyCaptureCount != 0) { + throw new IllegalStateException("pad-constant slot has pending legacy releases"); + } + transferWithoutRuntimeRoot(target, false); + } + + /** Record one package array/hash table entry as an identity-owned aggregate root. */ + synchronized void acquirePackageAggregateIdentity(RuntimeBase target) { + if (kind != Kind.PACKAGE_AGGREGATE_SLOT) { + throw new IllegalStateException("package aggregate acquisition requires an aggregate-slot identity"); + } + Objects.requireNonNull(target, "target"); + if (legacyCaptureCount != 0) { + throw new IllegalStateException("package aggregate slot has pending legacy releases"); + } + transferWithoutRuntimeRoot(target, true); + } + + /** Record one active lexical pad entry that owns an aggregate. */ + synchronized void acquireActiveLexicalIdentity(RuntimeBase target) { + if (kind != Kind.ACTIVE_LEXICAL) { + throw new IllegalStateException("active lexical acquisition requires an active-lexical slot"); + } + Objects.requireNonNull(target, "target"); + if (!(target instanceof RuntimeArray || target instanceof RuntimeHash)) { + throw new IllegalArgumentException("active lexical owner slots are for aggregate pads"); + } + if (legacyCaptureCount != 0) { + throw new IllegalStateException("active lexical slot has pending legacy releases"); + } + transferWithoutRuntimeRoot(target, true); + } + + /** Record a strong edge without promoting an untracked referent's legacy count. */ + synchronized void acquireUntrackedIdentity(RuntimeBase target) { + if (kind != Kind.UNTRACKED_REFERENCE) { + throw new IllegalStateException("untracked acquisition requires an untracked-reference slot"); + } + Objects.requireNonNull(target, "target"); + if (target.refCount == Integer.MIN_VALUE) { + throw new IllegalStateException("cannot own a destroyed referent"); + } + if (nativeOwner || legacyCaptureCount != 0) { + throw new IllegalStateException("untracked identity cannot replace native or capture ownership"); + } + transferWithoutRuntimeRoot(target, false); + } + + /** Identify one temporary compatibility count installed for weak tracking. */ + synchronized void acquireWeaklyTrackedCompatibilityPin(RuntimeBase target) { + if (kind != Kind.WEAKLY_TRACKED_COMPATIBILITY_PIN) { + throw new IllegalStateException("compatibility pin requires its dedicated owner-slot kind"); + } + Objects.requireNonNull(target, "target"); + if (target.refCount == Integer.MIN_VALUE) { + throw new IllegalStateException("cannot pin a destroyed referent"); + } + if (referent != null || nativeOwner || legacyCaptureCount != 0) { + throw new IllegalStateException("compatibility pin is already active"); + } + transferWithoutRuntimeRoot(target, false); + } + + /** Record one direct value retained by a runtime temporary root. */ + synchronized void acquireTemporaryRootIdentity(RuntimeBase target) { + if (kind != Kind.TEMPORARY_ROOT) { + throw new IllegalStateException("temporary-root acquisition requires a temporary-root slot"); + } + Objects.requireNonNull(target, "target"); + if (referent != null || nativeOwner || legacyCaptureCount != 0) { + throw new IllegalStateException("temporary-root slot is already active"); + } + if (target.refCount == Integer.MIN_VALUE) { + throw new IllegalStateException("cannot retain a destroyed temporary root"); + } + transferWithoutRuntimeRoot(target, false); + } + + private void transferWithoutRuntimeRoot(RuntimeBase target, boolean nextNativeOwner) { + if (referent == target) { + if (ownerRuntimeState != null) { + PerlRuntime.releaseOwnerSlotReferent(ownerRuntimeState, referent); + } + ownerRuntime = null; + ownerRuntimeState = null; + nativeOwner = nextNativeOwner; + return; + } + RuntimeBase previous = referent; + LifecycleRuntimeState previousState = ownerRuntimeState; + referent = target; + ownerRuntime = null; + ownerRuntimeState = null; + nativeOwner = nextNativeOwner; + if (previous != null && registeredInOwnerLedger) previous.removeOwnerSlot(this); + if (previous != null && registeredInOwnerLedger) { + PerlRuntime.releaseOwnerSlotReferent(previousState, previous); + } + registeredInOwnerLedger = true; + target.addOwnerSlot(this); + finishDeferredCodeRelease(previous); + finishDeferredScalarCellRelease(previous); + WeakRefRegistry.onStrongOwnerSlotReleased(previous); + } + + /** + * Record a scalar-capture identity held by its RuntimeCode owner. The + * closure already strongly owns the pad cell through capturedScalars, so + * registering a second runtime root would let an unreachable closure's + * slot outlive the closure and retain request resources. + */ + public synchronized void acquireCaptureIdentity(RuntimeBase target) { + acquireCaptureIdentity(target, null); + } + + /** Record a scalar-pad identity and the CODE object that owns the capture. */ + public synchronized void acquireCaptureIdentity(RuntimeBase target, RuntimeCode owner) { + if (kind != Kind.SCALAR_CAPTURE) { + throw new IllegalStateException("capture identity requires a scalar-capture slot"); + } + Objects.requireNonNull(target, "target"); + if (referent != null || nativeOwner) { + throw new IllegalStateException("capture identity is already active"); + } + if (target.refCount == Integer.MIN_VALUE) { + throw new IllegalStateException("cannot capture a destroyed pad cell"); + } + referent = target; + captureOwner = owner == null ? null : new WeakReference<>(owner); + registeredInOwnerLedger = true; + target.addOwnerSlot(this); + } + + synchronized RuntimeCode captureOwner() { + return captureOwner == null ? null : captureOwner.get(); + } + + synchronized void rebindCaptureOwner(RuntimeCode owner) { + if (kind != Kind.SCALAR_CAPTURE && kind != Kind.CLOSURE_PAD) { + throw new IllegalStateException("only closure-pad slots have a CODE owner"); + } + if (referent == null) return; + captureOwner = owner == null ? null : new WeakReference<>(owner); + } + + /** The active frame already retains the CV; no second global root is needed. */ + synchronized void acquireActiveCodeIdentity(RuntimeCode target) { + if (kind != Kind.ACTIVE_CODE || referent != null) { + throw new IllegalStateException("active CODE slot is not available"); + } + referent = Objects.requireNonNull(target, "target"); + nativeOwner = true; + registeredInOwnerLedger = true; + target.addOwnerSlot(this); + } + + /** Identify a counted scalar or element edge without adding a global root. */ + public synchronized void acquireCountedIdentity(RuntimeBase target) { + if (!isCountedKind(kind)) { + throw new IllegalStateException("counted acquisition requires a scalar or element slot"); + } + Objects.requireNonNull(target, "target"); + if (nativeOwner || legacyCaptureCount != 0) { + throw new IllegalStateException("counted identity cannot replace native or capture ownership"); + } + if (referent == target) return; + release(); + referent = target; + registeredInOwnerLedger = true; + target.addOwnerSlot(this); + if (kind == Kind.ARGUMENT_VALUE) { + ownerRuntime = PerlRuntime.currentOrNull(); + ownerRuntimeState = ownerRuntime == null + ? null : ownerRuntime.retainOwnerSlotReferent(target); + } + } + + /** + * Record an active {@code @_} alias to a scalar cell without adding a + * second Perl-visible reference count for the aliased value. + */ + synchronized void acquireArgumentAliasIdentity(RuntimeBase target) { + if (kind != Kind.ARGUMENT_ALIAS) { + throw new IllegalStateException("argument-alias acquisition requires an alias slot"); + } + Objects.requireNonNull(target, "target"); + if (target.refCount == Integer.MIN_VALUE) { + release(); + return; + } + if (nativeOwner || legacyCaptureCount != 0) { + throw new IllegalStateException("argument alias cannot replace native ownership"); + } + if (referent == target) return; + + // The call frame already strongly owns the scalar cell, which in turn + // owns its current value. Record the slot identity on the referent for + // cleanup decisions without adding a second runtime root. + RuntimeBase previous = referent; + LifecycleRuntimeState previousState = ownerRuntimeState; + referent = target; + ownerRuntime = null; + ownerRuntimeState = null; + if (previous != null && registeredInOwnerLedger) { + previous.removeOwnerSlot(this); + PerlRuntime.releaseOwnerSlotReferent(previousState, previous); + } + registeredInOwnerLedger = true; + target.addOwnerSlot(this); + finishDeferredScalarCellRelease(previous); + WeakRefRegistry.onStrongOwnerSlotReleased(previous); + } + + synchronized void reclassifyCountedIdentity(Kind nextKind) { + if (nativeOwner || !isCountedKind(kind) || !isCountedKind(nextKind)) { + throw new IllegalStateException("only counted scalar and element slots can move storage"); + } + if (kind != nextKind && referent != null) { + if (nextKind == Kind.ARGUMENT_VALUE) { + ownerRuntime = PerlRuntime.currentOrNull(); + ownerRuntimeState = ownerRuntime == null + ? null : ownerRuntime.retainOwnerSlotReferent(referent); + } else if (kind == Kind.ARGUMENT_VALUE) { + PerlRuntime.releaseOwnerSlotReferent(ownerRuntimeState, referent); + ownerRuntime = null; + ownerRuntimeState = null; + } + } + kind = nextKind; + } + + static boolean isCountedKind(Kind kind) { + return kind == Kind.SCALAR_CELL || kind == Kind.CODE_SLOT || kind == Kind.ARRAY_ELEMENT + || kind == Kind.HASH_ELEMENT || kind == Kind.ARGUMENT_VALUE + || kind == Kind.RETURN_VALUE || kind == Kind.TIE_HANDLER; + } + + /** Retire this edge and carry its identity through one deferred decrement. */ + public synchronized PendingRelease releaseCountedIdentity() { + if (referent == null) return null; + if (nativeOwner || legacyCaptureCount != 0) { + throw new IllegalStateException("counted release requires a counted identity"); + } + PendingRelease release = new PendingRelease(identity, ++nextReleaseSequence, kind, referent); + release(); + return release; + } + + /** Replace this native slot's referent while retaining the slot identity. */ + public synchronized void replaceNativeOwner(RuntimeBase nextReferent) { + if (!nativeOwner || referent == null) { + throw new IllegalStateException("cannot replace a slot that is not a native owner"); + } + Objects.requireNonNull(nextReferent, "nextReferent"); + if (nextReferent.refCount == Integer.MIN_VALUE) { + throw new IllegalStateException("cannot own a destroyed referent"); + } + transferTo(nextReferent); + } + + /** Read the current referent as a borrow; this does not acquire ownership. */ + public synchronized RuntimeBase borrowReferent() { + return referent; + } + /** Queue one closure-pad count release and retain its slot provenance. */ public synchronized PendingRelease deferLegacyCaptureRelease(RuntimeBase target) { if (legacyCaptureCount <= 0) return null; @@ -166,11 +552,15 @@ public synchronized void transferTo(RuntimeBase nextReferent) { ownerRuntime = nextRuntime; ownerRuntimeState = nextRuntime == null || nextReferent == null ? null : nextRuntime.retainOwnerSlotReferent(nextReferent); - if (previous != null) previous.removeOwnerSlot(this); - if (previous != null) { + if (previous != null && registeredInOwnerLedger) previous.removeOwnerSlot(this); + if (previous != null && registeredInOwnerLedger) { PerlRuntime.releaseOwnerSlotReferent(previousState, previous); } - if (nextReferent != null) nextReferent.addOwnerSlot(this); + registeredInOwnerLedger = nextReferent != null; + if (registeredInOwnerLedger) nextReferent.addOwnerSlot(this); + finishDeferredCodeRelease(previous); + finishDeferredScalarCellRelease(previous); + WeakRefRegistry.onStrongOwnerSlotReleased(previous); } /** Release this slot. Repeated releases are harmless. */ @@ -178,10 +568,41 @@ public synchronized void release() { RuntimeBase previous = referent; LifecycleRuntimeState previousState = ownerRuntimeState; referent = null; + countedScalarOwner = null; + captureOwner = null; ownerRuntime = null; ownerRuntimeState = null; - if (previous != null) previous.removeOwnerSlot(this); - if (previous != null) PerlRuntime.releaseOwnerSlotReferent(previousState, previous); + if (previous != null && registeredInOwnerLedger) previous.removeOwnerSlot(this); + if (previous != null && registeredInOwnerLedger) { + PerlRuntime.releaseOwnerSlotReferent(previousState, previous); + } + registeredInOwnerLedger = false; + finishDeferredCodeRelease(previous); + finishDeferredScalarCellRelease(previous); + WeakRefRegistry.onStrongOwnerSlotReleased(previous); + } + + private static void finishDeferredCodeRelease(RuntimeBase previous) { + if (previous instanceof RuntimeCode code) { + code.finishActiveCodeOwnerRelease(); + } } + /** Retry escaped scalar-cell cleanup after its final Perl owner leaves. */ + static void finishDeferredScalarCellRelease(RuntimeBase previous) { + if (!(previous instanceof RuntimeScalar scalar) + || !scalar.referencedByScalarReference + || scalar.perlLifecycleState() == RuntimeBase.PerlLifecycleState.DESTROYED + || scalar.refCount > 0 + || scalar.captureCount != 0 + || scalar.localBindingExists + || MyVarCleanupStack.isRegistered(scalar) + || scalar.hasScalarCellOwner() + || PerlRuntime.currentOrNull() == null) { + return; + } + RuntimeScalar.scopeExitCleanup(scalar); + } + + } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java index 506b7e6636..53743cd020 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java @@ -539,6 +539,10 @@ private RootSnapshot snapshotCloneInternal( sourceMapperState.snapshotInto(child.sourceMapperState); regexState.snapshotInto(child.regexState); } + try (Binding ignored = child.bind()) { + child.globalState.rebuildPackageScalarOwnerSlots(); + child.globalState.rebuildPackageAggregateOwnerSlots(); + } java.util.List clonedRoots = cloner.cloneSnapshotRoots(roots); cloner.finishSnapshot(); child.currentDirectory = currentDirectory; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ReachabilityWalker.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ReachabilityWalker.java index d5153b130e..ee852b40c2 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ReachabilityWalker.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ReachabilityWalker.java @@ -40,6 +40,12 @@ * */ public class ReachabilityWalker { + /** Runtime cleanup must use owner releases; retain walker diagnostics only. */ + private static final boolean CORRECTIVE_SWEEPS_DISABLED = true; + + static boolean correctiveSweepsEnabled() { + return !CORRECTIVE_SWEEPS_DISABLED; + } private static final String WALKER_TRACE_CLASS = System.getenv("JPERL_WALKER_TRACE_CLASS"); @@ -335,7 +341,6 @@ private void bfsEndBlockRoots(java.util.ArrayDeque todo) { visitScalar(value, todo); } } else if (cur instanceof RuntimeCode code) { - visitCodePadConstants(code, todo); visitCodeStateVariables(code, todo); if (code.closedOverVariables != null) { for (RuntimeBase captured : code.closedOverVariables.values()) { @@ -392,7 +397,6 @@ private void bfs(java.util.ArrayDeque todo, boolean walkCaptures) { if (!hasWalkableCodeEdges(code, walkCaptures)) { continue; } - visitCodePadConstants(code, todo); visitCodeStateVariables(code, todo); // Phase 2 normally keeps closure captures opaque to avoid // over-rescuing DBIC objects through internal callbacks. @@ -424,14 +428,6 @@ private void bfs(java.util.ArrayDeque todo, boolean walkCaptures) { } } - private void visitCodePadConstants(RuntimeCode code, - java.util.ArrayDeque todo) { - if (code.padConstants == null) return; - for (RuntimeBase constant : code.padConstants) { - addReachable(constant, todo); - } - } - /** * Global CODE slots themselves are roots, but most named subs have no * object-valued pad, state, or capture edges. Keep those CODE refs in the @@ -450,8 +446,7 @@ private void addGlobalCodeRoot(RuntimeScalar slot, java.util.ArrayDeque releasedTargets, ReleasedWeakSweepResult releasedSweepResult) { + if (CORRECTIVE_SWEEPS_DISABLED) return 0; return sweepWeakRefsPass(quiet, forceJvmGc, releasedTargets, releasedSweepResult).cleared(); } + /** Run one quiet pass explicitly requested at a safe statement boundary. */ + static int sweepWeakRefsForExplicitImmediateRequest(boolean forceJvmGc) { + if (CORRECTIVE_SWEEPS_DISABLED) return 0; + return sweepWeakRefsPass(true, forceJvmGc, Collections.emptySet(), null).cleared(); + } + private record WeakSweepPass(int cleared, boolean graphChanged) {} private static final int MAX_WITNESS_SWEEP_TARGETS = 64; @@ -2887,7 +2905,8 @@ private static boolean sameIdentityContents( } private static boolean hasExistingWeakRetentionRule(RuntimeBase referent) { - if (referent.hasSemanticCaptureOwner()) return true; + if (referent.hasSemanticCaptureOwner() || referent.hasCallTransferOwner() + || referent.hasScalarCellOwner()) return true; if ((referent instanceof RuntimeHash || referent instanceof RuntimeArray) && referent.localBindingExists) return true; if (referent instanceof RuntimeScalar scalar) { @@ -2899,7 +2918,8 @@ private static boolean hasExistingWeakRetentionRule(RuntimeBase referent) { } private static boolean hasExistingDestroyRetentionRule(RuntimeBase referent) { - return referent.hasSemanticCaptureOwner() + return referent.hasSemanticCaptureOwner() || referent.hasCallTransferOwner() + || referent.hasScalarCellOwner() || ((referent instanceof RuntimeHash || referent instanceof RuntimeArray) && referent.localBindingExists); } @@ -3012,15 +3032,21 @@ private static WeakSweepPass sweepWeakRefsPass( Set countedStrongOwners = releasedSweepResult == null ? Collections.emptySet() : releasedSweepResult.countedStrongOwners(); - WeakBackrefCaptureSnapshot weakBackrefCaptures = quiet - ? weakBackrefCaptureSnapshot() : null; for (RuntimeBase referent : WeakRefRegistry.snapshotWeakRefReferents()) { boolean liveReferent = live.contains(referent); traceWeakReferent(referent, liveReferent, strongCycleProtected.contains(referent)); // Semantic closure ownership is an explicit Perl edge. It is not // necessarily visible to this conservative graph walk because // generated closure fields and metadata are intentionally opaque. - if (!liveReferent && referent.hasSemanticCaptureOwner()) { + if (!liveReferent && (referent.hasSemanticCaptureOwner() + || referent.hasCallTransferOwner() || referent.hasScalarCellOwner())) { + continue; + } + // A live owner-slot identity is already the Perl refcount proof for + // this referent. In particular, a strong ARRAY_ELEMENT or + // HASH_ELEMENT slot protects a child even when the conservative + // root walk cannot see the generated closure that owns its parent. + if (!liveReferent && referent.hasOwnerSlots()) { continue; } if (!liveReferent && hasCapturedAggregateElementOwner(referent)) { @@ -3064,19 +3090,6 @@ private static WeakSweepPass sweepWeakRefsPass( continue; } } - // Quiet auto-sweeps can run while a deferred CODE ref is being - // returned to its caller. Sub::Quote stores weak registry - // entries in hashes/arrays that are strongly captured by that - // CODE, but the caller's lexical may not be visible as a root - // yet. Keep this exception limited to unblessed metadata - // containers and only for quiet statement-boundary sweeps; - // explicit jperl_gc() keeps the stricter root-based behavior. - if (quiet - && referent.blessId == 0 - && (referent instanceof RuntimeHash || referent instanceof RuntimeArray) - && weakBackrefCaptures.protects(referent)) { - continue; - } // Quiet auto-sweeps run at statement boundaries. Do not clear // weak refs into, or strongly reachable from, a strong cycle: // Perl's refcounting keeps such cycle islands alive. This @@ -3107,7 +3120,8 @@ private static WeakSweepPass sweepWeakRefsPass( if (live.contains(referent)) { continue; } - if (referent.hasSemanticCaptureOwner()) { + if (referent.hasSemanticCaptureOwner() || referent.hasCallTransferOwner() + || referent.hasScalarCellOwner()) { continue; } if (hasCapturedAggregateElementOwner(referent)) { @@ -3179,6 +3193,9 @@ private static void releaseUnreachableScopeExitedCaptureOwners( if (padCell == null || !padCell.scopeExited || slot.referent() != referent) { continue; } + + if (releaseExplicitCaptureOwners(padCell, referent, endReachable)) continue; + if (retainedByEnd == null) retainedByEnd = walkEndBlockRoots(); boolean padInEnd = retainedByEnd.contains(padCell); boolean referentInEnd = retainedByEnd.contains(referent); @@ -3206,6 +3223,51 @@ && findPathTo(padCell, false, false) == null) { } } + /** + * Retire scope-exited captures through their CODE owner slots when every + * capture of this pad has an explicit owner identity. Return false when a + * compatibility-only capture remains, so the legacy root proof can still + * protect it. + */ + private static boolean releaseExplicitCaptureOwners( + RuntimeScalar padCell, RuntimeBase referent, Set endReachable) { + ArrayList captureSlots = new ArrayList<>(); + for (PerlOwnerSlot captureSlot : padCell.ownerSlotSnapshot()) { + if (captureSlot.kind() == PerlOwnerSlot.Kind.SCALAR_CAPTURE + && captureSlot.referent() == padCell) { + captureSlots.add(captureSlot); + } + } + int padCaptureCount = padCell.captureCount; + if (captureSlots.isEmpty()) return false; + + boolean allCapturesExplicit = captureSlots.size() == padCaptureCount; + for (PerlOwnerSlot captureSlot : captureSlots) { + RuntimeCode captureOwner = captureSlot.captureOwner(); + if (captureOwner == null) { + allCapturesExplicit = false; + continue; + } + if (captureOwner.hasPerlOwnerForCapture()) continue; + if (endReachable != null + && (endReachable.contains(padCell) || endReachable.contains(referent))) continue; + if (WALKER_TRACE_CLASS != null && !WALKER_TRACE_CLASS.isEmpty()) { + String className = NameNormalizer.getBlessStr(referent.blessId); + if (className != null && className.contains(WALKER_TRACE_CLASS)) { + System.err.println("[WALKER_CAPTURE_RELEASE] target=" + className + "@" + + System.identityHashCode(referent) + + " slot=" + captureSlot.identity() + + " pad=" + System.identityHashCode(padCell) + + " captureCount=" + padCaptureCount + + " owner=" + System.identityHashCode(captureOwner) + + " ownerless=true"); + } + } + captureOwner.releaseCaptures(); + } + return allCapturesExplicit; + } + private static boolean containsAnyCandidate( RuntimeBase root, Set candidates) { Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); @@ -3313,13 +3375,26 @@ public static ReleasedWeakSweepResult sweepReleasedWeakReferents( Collections.emptySet(), 0); } - // A registered, live, nonweak scalar with an owned direct reference is - // itself a root in walk(). If every released target has such an owner, - // no graph traversal is needed to preserve those targets. This is a - // deliberately narrow proof: nested container slots, exited captures, - // and uncounted references still use the full reachability walk below. + // A registered, live, nonweak scalar with an owned direct reference, + // or any active explicit owner slot, is a direct Perl strong-owner + // proof. If every released target has one, no graph traversal is + // needed to preserve those targets. Uncounted references still use + // the full reachability walk below. Set directlyOwned = Collections.newSetFromMap(new IdentityHashMap<>()); + for (RuntimeBase referent : pending) { + if (referent.hasDirectOwnerSlotProofForReleasedWeakReferent()) { + directlyOwned.add(referent); + } + } + if (directlyOwned.containsAll(pending)) { + return new ReleasedWeakSweepResult(null, Collections.emptySet(), + directlyOwned, 0); + } + + // Owner-slot identities cover the migrated scalar, element, capture, + // and call-transfer families. Only take the legacy scalar registry + // snapshot for targets that still need its compatibility proof. for (RuntimeScalar owner : ScalarRefRegistry.snapshot()) { if (owner == null || !owner.refCountOwned || owner.scopeExited || !MyVarCleanupStack.isLive(owner) || WeakRefRegistry.isweak(owner) @@ -3401,7 +3476,8 @@ public static ReleasedWeakSweepResult sweepReleasedWeakReferents( // conservative root walk cannot see the generated closure fields. // Match sweepWeakRefs() above: a targeted release of another edge // must not destroy an aggregate still captured by a live closure. - if (referent.hasSemanticCaptureOwner()) continue; + if (referent.hasOwnerSlots() || referent.hasSemanticCaptureOwner() + || referent.hasCallTransferOwner() || referent.hasScalarCellOwner()) continue; // A targeted release only means that one explicit owner went // away. Other counted owners can still hold the referent through // local aggregate slots that are not represented in the lexical @@ -3467,6 +3543,7 @@ public static ReleasedWeakSweepResult sweepReleasedWeakReferents( * DBI statement-polling paths without forcing a JVM GC. */ public static int sweepDestroyableObjects(boolean forceJvmGc) { + if (CORRECTIVE_SWEEPS_DISABLED) return 0; if (!DestroyDispatch.hasDestroyableObjects()) return 0; if (forceJvmGc) { ScalarRefRegistry.forceGcAndSnapshot(); @@ -3477,6 +3554,7 @@ public static int sweepDestroyableObjects(boolean forceJvmGc) { /** Reconcile File::Temp-style external resources at a safe statement boundary. */ static int sweepStatementBoundaryDestroyableObjects() { + if (CORRECTIVE_SWEEPS_DISABLED) return 0; java.util.List candidates = DestroyDispatch.snapshotStatementBoundaryDestroyableObjects(); if (candidates.isEmpty()) return 0; @@ -3604,4 +3682,9 @@ private static WeakBackrefCaptureSnapshot weakBackrefCaptureSnapshot() { && todo.isEmpty() && visits < MAX_WEAK_BACKREF_CAPTURE_VISITS; return new WeakBackrefCaptureSnapshot(seen, complete); } + + /** Record the legacy post-DESTROY capture diagnostic without cleaning objects. */ + static void inspectWeakBackrefCapturesForDiagnostics() { + weakBackrefCaptureSnapshot(); + } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java index 369ecc7732..4b0d0f5978 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java @@ -190,7 +190,12 @@ Object snapshotRegexMutationState() { void restoreRegexMutationState(Object token) { if (!(token instanceof RegexMutationState state)) return; deferElementsAddedSince(state.elements); + List replaced = new ArrayList<>(elements); elements = newElementList(state.elements); + for (RuntimeScalar value : replaced) { + if (value != null) value.reconcileContainerOwnerAfterMutation(this); + } + refreshUntrackedElementOwners(); type = state.type; strictAutovivify = state.strictAutovivify; scalarContextSize = state.scalarContextSize; @@ -245,9 +250,12 @@ public boolean add(RuntimeScalar value) { } owner.noteIsaMutation(); owner.notePackageRootMutation(null, value); - if (value != null) value.markContainerOwner(owner); - owner.markPackageRootedValue(value); boolean changed = super.add(value); + if (changed) { + if (value != null) value.markContainerOwner(owner); + owner.markPackageRootedValue(value); + refreshStoredUntrackedOwner(value); + } owner.validateIsaMutation(); return changed; } @@ -260,9 +268,10 @@ public void add(int index, RuntimeScalar element) { } owner.noteIsaMutation(); owner.notePackageRootMutation(null, element); + super.add(index, element); if (element != null) element.markContainerOwner(owner); owner.markPackageRootedValue(element); - super.add(index, element); + refreshStoredUntrackedOwner(element); owner.validateIsaMutation(); } @@ -271,11 +280,14 @@ public boolean addAll(java.util.Collection c) { if (!owner.threadShared) { if (!c.isEmpty()) owner.noteIsaMutation(); owner.notePackageRootMutationIf(owner.hasRootEdge(c)); - for (RuntimeScalar value : c) { - if (value != null) value.markContainerOwner(owner); - owner.markPackageRootedValue(value); - } boolean changed = super.addAll(c); + if (changed) { + for (RuntimeScalar value : c) { + if (value != null) value.markContainerOwner(owner); + owner.markPackageRootedValue(value); + refreshStoredUntrackedOwner(value); + } + } if (changed) owner.validateIsaMutation(); return changed; } @@ -292,11 +304,14 @@ public boolean addAll(int index, java.util.Collection c if (!owner.threadShared) { if (!c.isEmpty()) owner.noteIsaMutation(); owner.notePackageRootMutationIf(owner.hasRootEdge(c)); - for (RuntimeScalar value : c) { - if (value != null) value.markContainerOwner(owner); - owner.markPackageRootedValue(value); - } boolean changed = super.addAll(index, c); + if (changed) { + for (RuntimeScalar value : c) { + if (value != null) value.markContainerOwner(owner); + owner.markPackageRootedValue(value); + refreshStoredUntrackedOwner(value); + } + } if (changed) owner.validateIsaMutation(); return changed; } @@ -318,19 +333,55 @@ public RuntimeScalar set(int index, RuntimeScalar element) { } owner.noteIsaMutation(); owner.notePackageRootMutation(previous, element); + RuntimeScalar result = super.set(index, element); if (element != null) element.markContainerOwner(owner); owner.markPackageRootedValue(element); - RuntimeScalar result = super.set(index, element); + refreshStoredUntrackedOwner(element); + if (previous != element) { + if (previous != null) { + previous.reconcileContainerOwnerAfterMutation(owner); + } + RuntimeCode.argumentArraySlotRemoved(owner, previous); + } owner.validateIsaMutation(); return result; } + private void refreshStoredUntrackedOwner(RuntimeScalar value) { + // Copy construction populates a replacement list before assigning + // it to owner.elements. Defer refresh until this list is installed. + if (value != null && owner.elements == this) { + if (owner.isPackageGlobalRoot) { + value.markPrepopulatedPackageAggregateOwner(owner); + } + value.refreshUntrackedReferenceOwner(); + } + } + @Override public RuntimeScalar remove(int index) { RuntimeScalar previous = super.remove(index); + if (previous != null) previous.reconcileContainerOwnerAfterMutation(owner); owner.noteIsaMutation(); owner.notePackageRootMutation(previous, null); owner.validateIsaMutation(); + RuntimeCode.argumentArraySlotRemoved(owner, previous); + return previous; + } + + @Override + public RuntimeScalar removeFirst() { + RuntimeScalar previous = super.removeFirst(); + if (previous != null) previous.reconcileContainerOwnerAfterMutation(owner); + RuntimeCode.argumentArraySlotRemoved(owner, previous); + return previous; + } + + @Override + public RuntimeScalar removeLast() { + RuntimeScalar previous = super.removeLast(); + if (previous != null) previous.reconcileContainerOwnerAfterMutation(owner); + RuntimeCode.argumentArraySlotRemoved(owner, previous); return previous; } @@ -338,9 +389,11 @@ public RuntimeScalar remove(int index) { public boolean remove(Object o) { boolean removed = super.remove(o); if (removed && o instanceof RuntimeScalar scalar) { + scalar.reconcileContainerOwnerAfterMutation(owner); owner.noteIsaMutation(); owner.notePackageRootMutation(scalar, null); owner.validateIsaMutation(); + RuntimeCode.argumentArraySlotRemoved(owner, scalar); } return removed; } @@ -348,12 +401,83 @@ public boolean remove(Object o) { @Override public void clear() { if (!isEmpty()) { + RuntimeScalar[] removed = toArray(new RuntimeScalar[0]); owner.noteIsaMutation(); owner.notePackageRootClear(this); + for (RuntimeScalar scalar : removed) { + RuntimeCode.argumentArraySlotRemoved(owner, scalar); + } + super.clear(); + for (RuntimeScalar scalar : removed) { + if (scalar != null) scalar.reconcileContainerOwnerAfterMutation(owner); + } + owner.validateIsaMutation(); + return; } super.clear(); owner.validateIsaMutation(); } + + @Override + protected void removeRange(int fromIndex, int toIndex) { + List removed = new ArrayList<>(subList(fromIndex, toIndex)); + for (int i = fromIndex; i < toIndex; i++) { + RuntimeCode.argumentArraySlotRemoved(owner, get(i)); + } + super.removeRange(fromIndex, toIndex); + for (RuntimeScalar scalar : removed) { + if (scalar != null) scalar.reconcileContainerOwnerAfterMutation(owner); + } + } + + @Override + public boolean removeIf(java.util.function.Predicate filter) { + Objects.requireNonNull(filter); + boolean changed = false; + Iterator iterator = iterator(); + while (iterator.hasNext()) { + RuntimeScalar removed = iterator.next(); + if (filter.test(removed)) { + iterator.remove(); + if (removed != null) removed.reconcileContainerOwnerAfterMutation(owner); + changed = true; + } + } + return changed; + } + + @Override + public boolean removeAll(java.util.Collection values) { + Objects.requireNonNull(values); + return removeMatching(values, false); + } + + @Override + public boolean retainAll(java.util.Collection values) { + Objects.requireNonNull(values); + return removeMatching(values, true); + } + + private boolean removeMatching(java.util.Collection values, boolean retainMatches) { + java.util.List membership = new ArrayList<>(values); + boolean changed = false; + for (int index = size() - 1; index >= 0; index--) { + RuntimeScalar value = get(index); + if (membership.contains(value) != retainMatches) { + remove(index); + changed = true; + } + } + return changed; + } + + @Override + public void replaceAll(java.util.function.UnaryOperator operator) { + Objects.requireNonNull(operator); + for (int i = 0; i < size(); i++) { + set(i, operator.apply(get(i))); + } + } } /** Compact backing for the common one-argument method call plus invocant. */ @@ -395,6 +519,11 @@ public RuntimeScalar set(int index, RuntimeScalar element) { second = element; result = previous; } + if (element != null) element.markContainerOwner(owner); + owner.markPackageRootedValue(element); + if (previous != element && previous != null) { + previous.reconcileContainerOwnerAfterMutation(owner); + } owner.validateIsaMutation(); return result; } @@ -424,6 +553,8 @@ public void add(int index, RuntimeScalar element) { first = null; second = null; } + if (element != null) element.markContainerOwner(owner); + owner.markPackageRootedValue(element); modCount++; owner.validateIsaMutation(); } @@ -447,12 +578,12 @@ public boolean addAll(int index, Collection values) { } owner.noteIsaMutation(); owner.notePackageRootMutationIf(owner.hasRootEdge(source)); + int insertionIndex = index; + for (RuntimeScalar value : source) addRaw(insertionIndex++, value); for (RuntimeScalar value : source) { if (value != null) value.markContainerOwner(owner); owner.markPackageRootedValue(value); } - int insertionIndex = index; - for (RuntimeScalar value : source) addRaw(insertionIndex++, value); modCount++; owner.validateIsaMutation(); return true; @@ -496,6 +627,7 @@ public RuntimeScalar remove(int index) { inlineSize--; } modCount++; + if (previous != null) previous.reconcileContainerOwnerAfterMutation(owner); owner.noteIsaMutation(); owner.notePackageRootMutation(previous, null); owner.validateIsaMutation(); @@ -505,6 +637,8 @@ public RuntimeScalar remove(int index) { @Override public void clear() { if (isEmpty()) return; + List removed = new ArrayList<>(); + for (int i = 0; i < size(); i++) removed.add(get(i)); owner.noteIsaMutation(); owner.notePackageRootClear(this); if (expanded != null) expanded.clear(); @@ -512,6 +646,9 @@ public void clear() { second = null; inlineSize = 0; modCount++; + for (RuntimeScalar scalar : removed) { + if (scalar != null) scalar.reconcileContainerOwnerAfterMutation(owner); + } owner.validateIsaMutation(); } @@ -522,8 +659,6 @@ private void prepareStoredValue(RuntimeScalar previous, RuntimeScalar element) { } owner.noteIsaMutation(); owner.notePackageRootMutation(previous, element); - if (element != null) element.markContainerOwner(owner); - owner.markPackageRootedValue(element); } } @@ -590,15 +725,31 @@ void notePackageRootMutation() { } void markPackageRootedValue(RuntimeScalar value) { - if (isPackageGlobalRoot && value != null) { + if (isPackageGlobalRoot && !elementsAliased && value != null) { value.isPackageGlobalRoot = true; + value.markContainerOwner(this); + if (value.value instanceof RuntimeBase referent) { + referent.storedInPackageGlobal = true; + } } } private void markPackageRootedValues(int fromIndex) { if (!isPackageGlobalRoot) return; for (int i = Math.max(0, fromIndex); i < elements.size(); i++) { - markPackageRootedValue(elements.get(i)); + RuntimeScalar value = elements.get(i); + markPackageRootedValue(value); + if (value != null) { + value.markPrepopulatedPackageAggregateOwner(this); + value.refreshUntrackedReferenceOwner(); + } + } + } + + private void refreshUntrackedElementOwners() { + if (elements == null) return; + for (RuntimeScalar value : elements) { + if (value != null) value.reconcileContainerOwnerAfterMutation(this); } } @@ -617,6 +768,17 @@ public boolean ownsElement(RuntimeScalar scalar) { return ownedAliasElements != null && ownedAliasElements.contains(scalar); } + /** Whole-array replacement releases owned SVs, preserving borrowed caller cells. */ + private void deferOwnedElementReleases() { + if (!elementsAliased) { + MortalList.deferDestroyForContainerClear(elements); + } else if (ownedAliasElements != null) { + MortalList.deferDestroyForContainerClear(new ArrayList<>(ownedAliasElements)); + ownedAliasElements = null; + elementsOwned = false; + } + } + public void forgetOwnedAliasElement(RuntimeScalar scalar) { if (ownedAliasElements == null || scalar == null) return; ownedAliasElements.remove(scalar); @@ -638,6 +800,7 @@ public void forgetOwnedAliasElement(RuntimeScalar scalar) { */ public RuntimeArray(List list) { this.elements = newElementList(list); + refreshUntrackedElementOwners(); } public RuntimeArray(RuntimeBase... values) { @@ -695,13 +858,12 @@ public static RuntimeScalar pop(RuntimeArray runtimeArray) { // so its elements must NOT be mortal-ized on shift/pop — doing so // would corrupt the caller's refCount tracking. if (runtimeArray.ownsElement(result) && result.refCountOwned - && (result.type & RuntimeScalarType.REFERENCE_BIT) != 0 + && ((result.type & RuntimeScalarType.REFERENCE_BIT) != 0 + || result.type == RuntimeScalarType.GLOB) && result.value instanceof RuntimeBase base && base.refCount > 0) { result.refCountOwned = false; runtimeArray.forgetOwnedAliasElement(result); - base.releaseActiveOwner(result); - MortalList.requestWeakSweepsForDestroyedContainer(base); MortalList.deferDecrementFromArraySlot( base, result, "RuntimeArray.pop"); } @@ -738,13 +900,12 @@ public static RuntimeScalar shift(RuntimeArray runtimeArray) { // If this element owned a refCount, defer the decrement. // See pop() for rationale and elementsOwned guard. if (runtimeArray.ownsElement(result) && result.refCountOwned - && (result.type & RuntimeScalarType.REFERENCE_BIT) != 0 + && ((result.type & RuntimeScalarType.REFERENCE_BIT) != 0 + || result.type == RuntimeScalarType.GLOB) && result.value instanceof RuntimeBase base && base.refCount > 0) { result.refCountOwned = false; runtimeArray.forgetOwnedAliasElement(result); - base.releaseActiveOwner(result); - MortalList.requestWeakSweepsForDestroyedContainer(base); MortalList.deferDecrementFromArraySlot( base, result, "RuntimeArray.shift"); } @@ -1597,7 +1758,7 @@ public RuntimeArray set(RuntimeScalar value) { } this.eachIteratorIndex = null; notePackageRootMutation(); - MortalList.deferDestroyForContainerClear(this.elements); + this.deferOwnedElementReleases(); this.elements.clear(); this.ownedAliasElements = null; this.elementsAliased = false; @@ -1630,7 +1791,7 @@ public RuntimeArray setFromList(RuntimeList list) { } int originalSize = this.elements.size(); - MortalList.deferDestroyForContainerClear(this.elements); + this.deferOwnedElementReleases(); if (needsCopy) { // Make a defensive copy of the list before clearing RuntimeList listCopy = new RuntimeList(); @@ -1737,7 +1898,7 @@ public RuntimeArray setFromList(RuntimeList list) { * Behavior: *

    *
  1. Defer-decrement any existing counted elements (like normal {@code setFromList}).
  2. - *
  3. Copy new elements in without incrementing their referents' refCounts.
  4. + *
  5. Store the source scalar cells directly, without creating new ref owners.
  6. *
  7. Mark {@code elementsOwned=false} so {@link #shift(RuntimeArray)} * and other removal paths don't defer a spurious decrement.
  8. *
@@ -1750,19 +1911,23 @@ public RuntimeArray setFromListAliased(RuntimeList list) { } this.eachIteratorIndex = null; notePackageRootMutation(); - MortalList.deferDestroyForContainerClear(this.elements); + this.deferOwnedElementReleases(); this.elements.clear(); - list.addToArray(this); - // Elements are aliases: mark as non-owning. setLarge in later - // overwrites will still work correctly because setLarge checks - // refCountOwned before decrementing. - for (RuntimeScalar elem : this.elements) { - if (elem != null) elem.refCountOwned = false; - markPackageRootedValue(elem); - } - this.elementsOwned = false; + // This array is a view of existing SV cells. Preserve their identity + // so updates and source-cell cleanup are visible through the alias; + // copying each scalar here creates an extra untracked reference owner + // whose lifetime can outlast the source argument slot. this.elementsAliased = true; + this.elementsOwned = false; this.ownedAliasElements = null; + for (RuntimeBase value : list.elements) { + if (value instanceof RuntimeScalar scalar) { + this.elements.add(scalar); + } else if (value != null) { + value.addToArray(this); + } + } + list.elements.clear(); return this; } @@ -1779,7 +1944,7 @@ public RuntimeArray setFromReferenceList(RuntimeList list) { RuntimeArray references = new RuntimeArray(); references.setFromList(list); notePackageRootMutation(); - MortalList.deferDestroyForContainerClear(this.elements); + this.deferOwnedElementReleases(); this.elements.clear(); for (RuntimeScalar reference : references.elements) { // This is refaliasing, not an ordinary dereference. In @@ -2365,7 +2530,7 @@ public RuntimeArray undefine() { } notePackageRootMutation(); markScalarLocalContainerCleared(); - MortalList.deferDestroyForContainerClear(this.elements); + this.deferOwnedElementReleases(); if (elementSlotsAliasedIntoCallFrame) { for (RuntimeScalar element : this.elements) { if (element != null @@ -2509,7 +2674,7 @@ public void dynamicRestoreState() { // Without this, `local @_ = ($obj)` where $obj is tracked would // leak refCounts because the local elements are replaced without // ever going through scopeExitCleanup. - MortalList.deferDestroyForContainerClear(this.elements); + this.deferOwnedElementReleases(); // Real Perl semantics: `local @arr` creates a fresh temporary AV // for the scope; `bless \@arr, 'X'` blesses that temporary; at // local-restore, the temporary is freed and DESTROY fires for @@ -2540,6 +2705,8 @@ public void dynamicRestoreState() { this.elements = previousState.elements; } else { this.elements = newElementList(previousState.elements); + refreshUntrackedElementOwners(); + previousState.elements.clear(); } markPackageRootedValues(0); // Restore the type from the saved state (important for tied arrays) @@ -2568,6 +2735,7 @@ public void dynamicResumeState(Object token) { this.blessId = activeState.blessId; this.scalarContextSize = activeState.scalarContextSize; this.elements.addAll(activeState.elements); + activeState.elements.clear(); } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeBase.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeBase.java index 553ac10c8b..88e76192ce 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeBase.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeBase.java @@ -70,6 +70,12 @@ public static final class SharedLifecycle { volatile String publishedBlessName; } + enum SharedDestroyDisposition { + CONTINUE, + DEFER, + CLEAR_LOCAL_VIEW + } + /** * Refresh an unchanged local shared view after another runtime explicitly * publishes a class through shared scalar/aggregate storage. A view that @@ -116,12 +122,12 @@ void registerSharedFetchedView() { } } - /** - * @return true when ordinary DESTROY dispatch must be suppressed. - */ - boolean deferSharedDestroy() { + /** Decide whether this release owns canonical destruction or only a local view. */ + SharedDestroyDisposition sharedDestroyDisposition() { SharedLifecycle lifecycle = threadSharedLifecycle; - if (!threadShared || lifecycle == null || !lifecycle.active) return false; + if (!threadShared || lifecycle == null || !lifecycle.active) { + return SharedDestroyDisposition.CONTINUE; + } if (threadSharedFetchedView || threadSharedRuntimeView) { int remaining = lifecycle.liveViews.get(); @@ -133,14 +139,23 @@ boolean deferSharedDestroy() { } if (remaining == 0 && lifecycle.destroyPending.get() && lifecycle.destroyFired.compareAndSet(false, true)) { - return false; + return SharedDestroyDisposition.CONTINUE; } - return true; + // A fetch proxy has a Perl-local identity even though its storage + // is shared. Retire only its local weak observers; the canonical + // object's DESTROY and element cleanup still belong to the + // shared lifecycle. Runtime views retain the existing deferred + // destruction behavior until their lifecycle is separated too. + return threadSharedFetchedView + ? SharedDestroyDisposition.CLEAR_LOCAL_VIEW + : SharedDestroyDisposition.DEFER; } lifecycle.destroyPending.set(true); - if (lifecycle.liveViews.get() > 0) return true; - return !lifecycle.destroyFired.compareAndSet(false, true); + if (lifecycle.liveViews.get() > 0) return SharedDestroyDisposition.DEFER; + return lifecycle.destroyFired.compareAndSet(false, true) + ? SharedDestroyDisposition.CONTINUE + : SharedDestroyDisposition.DEFER; } // Index to the class that this reference belongs public int blessId; @@ -191,6 +206,13 @@ boolean deferSharedDestroy() { */ public boolean storedInPackageGlobal = false; + /** Compatibility count pin installed for an untracked package-rooted aggregate. */ + PerlOwnerSlot packageRootRescueOwnerSlot; + boolean packageRootRescuedUntrackedAggregate = false; + + /** Marks a counted referent kept alive only until its uncounted owner slots leave. */ + boolean deferredFinalReleaseForUncountedOwners = false; + /** * Number of live hash-element slots that directly own this referent. * This is separate from selective refCount because a returned method @@ -237,6 +259,11 @@ public void retainClosureCapture() { /** Retain one aggregate pad capture through an explicit owner slot. */ public PerlOwnerSlot retainClosureCaptureOwner() { + return retainClosureCaptureOwner(null); + } + + /** Retain one aggregate pad capture and identify its owning CODE object. */ + public PerlOwnerSlot retainClosureCaptureOwner(RuntimeCode captureOwner) { // A closure-pad slot is a semantic Perl owner even though it does not // contribute to the legacy selective refcount. A targeted weak sweep // can therefore leave an unblessed aggregate's legacy count at the @@ -248,7 +275,7 @@ public PerlOwnerSlot retainClosureCaptureOwner() { refCount = 0; } PerlOwnerSlot ownerSlot = new PerlOwnerSlot(PerlOwnerSlot.Kind.CLOSURE_PAD); - ownerSlot.acquireCapture(this); + ownerSlot.acquireCapture(this, captureOwner); retainClosureCapture(); return ownerSlot; } @@ -315,17 +342,199 @@ private void releaseClosureCapture(boolean nativeOwnerSlot) { /** Identity ledger for migrated slots; selective refCount remains the * lifecycle bridge while owner families move to this API. */ private java.util.Set ownerSlots = null; + private int argumentAliasOwnerSlots; + private int countedOwnerSlots; + private int pendingReturnOwners; + private int pendingOwnerReleases; + + synchronized boolean hasPendingReturnOwner() { + return pendingReturnOwners != 0; + } + + /** A queued Perl owner release has retired its live slot but not its count yet. */ + synchronized boolean hasPendingOwnerRelease() { + return pendingOwnerReleases != 0; + } + + /** Explicit call-transfer owners remain authoritative during the legacy bridge. */ + synchronized boolean hasCallTransferOwner() { + return pendingReturnOwners != 0 + || ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE) != 0; + } + + /** A referenced scalar cell is owned by its lexical binding or capture slots. */ + boolean hasScalarCellOwner() { + return this instanceof RuntimeScalar scalar && scalar.referencedByScalarReference + && (scalar.localBindingExists + || hasOwnerSlots() || hasPendingReturnOwner()); + } + + private synchronized void registerPendingOwnerRelease( + PerlOwnerSlot.PendingRelease release) { + pendingOwnerReleases++; + if (release != null && release.kind() == PerlOwnerSlot.Kind.RETURN_VALUE) { + pendingReturnOwners++; + } + } synchronized void addOwnerSlot(PerlOwnerSlot slot) { if (ownerSlots == null) { ownerSlots = java.util.Collections.newSetFromMap( new java.util.IdentityHashMap<>()); } - ownerSlots.add(slot); + if (ownerSlots.add(slot)) { + if (slot.kind() == PerlOwnerSlot.Kind.ARGUMENT_ALIAS) argumentAliasOwnerSlots++; + if (PerlOwnerSlot.isCountedKind(slot.kind())) countedOwnerSlots++; + } } synchronized void removeOwnerSlot(PerlOwnerSlot slot) { - if (ownerSlots != null) ownerSlots.remove(slot); + if (ownerSlots != null && ownerSlots.remove(slot)) { + if (slot.kind() == PerlOwnerSlot.Kind.ARGUMENT_ALIAS) argumentAliasOwnerSlots--; + if (PerlOwnerSlot.isCountedKind(slot.kind())) countedOwnerSlots--; + } + } + + synchronized boolean hasOwnerSlots() { + return ownerSlots != null && !ownerSlots.isEmpty(); + } + + /** + * Whether the legacy count or a direct Perl owner slot proves this value + * is still owned. Runtime temporary roots are evaluation scaffolding, not + * Perl references, and are deliberately excluded. + */ + public boolean hasPerlStrongOwner() { + return refCount > 0 || hasPerlStrongOwnerIdentity(); + } + + /** + * Whether current Perl owner identities, apart from the legacy count, + * prove that this value is still owned. Temporary roots and compatibility + * count pins are not Perl owners. + */ + boolean hasPerlStrongOwnerIdentity() { + if (localBindingExists || captureCount > 0 || hasPendingReturnOwner()) { + return true; + } + for (PerlOwnerSlot slot : ownerSlotSnapshot()) { + if (slot.isActive() + && slot.kind() != PerlOwnerSlot.Kind.TEMPORARY_ROOT + && slot.kind() != PerlOwnerSlot.Kind.WEAKLY_TRACKED_COMPATIBILITY_PIN) { + return true; + } + } + return false; + } + + /** Whether a non-counted owner slot proves Perl ownership of this value. */ + boolean hasUncountedPerlOwnerSlotIdentity() { + for (PerlOwnerSlot slot : ownerSlotSnapshot()) { + if (slot.isActive() + && !PerlOwnerSlot.isCountedKind(slot.kind()) + && slot.kind() != PerlOwnerSlot.Kind.TEMPORARY_ROOT + && slot.kind() != PerlOwnerSlot.Kind.WEAKLY_TRACKED_COMPATIBILITY_PIN) { + return true; + } + } + return false; + } + + boolean hasDirectOwnerSlotProofForReleasedWeakReferent() { + for (PerlOwnerSlot slot : ownerSlotSnapshot()) { + if (!slot.isActive()) continue; + if (slot.kind() == PerlOwnerSlot.Kind.WEAKLY_TRACKED_COMPATIBILITY_PIN) continue; + if (slot.kind() == PerlOwnerSlot.Kind.ARRAY_ELEMENT + || slot.kind() == PerlOwnerSlot.Kind.HASH_ELEMENT) { + RuntimeScalar scalar = slot.countedScalarOwner(); + if (scalar != null && scalar.getContainerOwner() == null) continue; + } + if (slot.kind() == PerlOwnerSlot.Kind.UNTRACKED_REFERENCE) { + RuntimeScalar scalar = slot.untrackedScalarOwner(); + if (scalar != null && scalar.hasPackageAggregateUntrackedReferenceOwner()) { + continue; + } + } + return true; + } + return false; + } + + /** + * Whether a durable scalar value owns this referent outside its count. + * Refresh each exact owner first so a stale scalar cell cannot pin a + * referent after that cell has lost its Perl-visible slot. + */ + boolean hasUntrackedReferenceOwnerSlotIdentity() { + for (PerlOwnerSlot slot : ownerSlotSnapshot()) { + if (slot.kind() != PerlOwnerSlot.Kind.UNTRACKED_REFERENCE || !slot.isActive()) continue; + RuntimeScalar owner = slot.untrackedScalarOwner(); + if (owner != null) { + if (owner.hasLiveDurableStrongValueSlot()) { + owner.refreshUntrackedReferenceOwner(); + } else { + owner.releaseUntrackedReferenceOwner(this); + } + } else { + slot.release(); + WeakRefRegistry.onStrongOwnerSlotReleased(this); + } + } + return ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE) != 0; + } + + boolean hasUntrackedReferenceOwnerSlotIdentity(int scalarType) { + if (!hasUntrackedReferenceOwnerSlotIdentity()) return false; + for (PerlOwnerSlot slot : ownerSlotSnapshot()) { + if (slot.kind() != PerlOwnerSlot.Kind.UNTRACKED_REFERENCE || !slot.isActive()) continue; + RuntimeScalar owner = slot.untrackedScalarOwner(); + if (owner != null && owner.type == scalarType + && owner.hasLiveDurableStrongValueSlot() + && !owner.hasPackageAggregateUntrackedReferenceOwner()) { + return true; + } + } + return false; + } + + /** Whether an aggregate has a surviving owner outside the legacy count. */ + boolean hasUncountedAggregateOwnerSlotIdentity() { + return (this instanceof RuntimeArray || this instanceof RuntimeHash) + && hasUncountedPerlOwnerSlotIdentity(); + } + + boolean hasNonPackageAggregateUncountedOwnerSlotIdentity() { + if (!(this instanceof RuntimeArray || this instanceof RuntimeHash)) return false; + for (PerlOwnerSlot slot : ownerSlotSnapshot()) { + if (!slot.isActive() + || PerlOwnerSlot.isCountedKind(slot.kind()) + || slot.kind() == PerlOwnerSlot.Kind.TEMPORARY_ROOT + || slot.kind() == PerlOwnerSlot.Kind.WEAKLY_TRACKED_COMPATIBILITY_PIN) { + continue; + } + if (slot.kind() == PerlOwnerSlot.Kind.UNTRACKED_REFERENCE) { + RuntimeScalar scalar = slot.untrackedScalarOwner(); + if (scalar != null && scalar.hasPackageAggregateUntrackedReferenceOwner()) { + continue; + } + } + return true; + } + return false; + } + + void deferFinalReleaseToUncountedOwnerSlots() { + if (!hasUncountedPerlOwnerSlotIdentity()) return; + refCount = WeakRefRegistry.WEAKLY_TRACKED; + deferredFinalReleaseForUncountedOwners = true; + } + + /** Check whether a live owner identity besides the slot being replaced remains. */ + boolean hasOwnerSlotsOtherThan(PerlOwnerSlot excluded) { + for (PerlOwnerSlot slot : ownerSlotSnapshot()) { + if (slot != excluded && slot.isActive()) return true; + } + return false; } public boolean hasSemanticCaptureOwner() { @@ -334,6 +543,7 @@ public boolean hasSemanticCaptureOwner() { /** Number of active slots of the requested kind that own this referent. */ public synchronized int ownerSlotCount(PerlOwnerSlot.Kind kind) { + if (kind == PerlOwnerSlot.Kind.ARGUMENT_ALIAS) return argumentAliasOwnerSlots; if (ownerSlots == null) return 0; int count = 0; for (PerlOwnerSlot slot : ownerSlots) { @@ -342,6 +552,45 @@ public synchronized int ownerSlotCount(PerlOwnerSlot.Kind kind) { return count; } + /** Number of active slot identities whose releases balance refCount. */ + synchronized int countedOwnerSlotCount() { + return countedOwnerSlots; + } + + /** Restore the compatibility count from surviving counted owner identities. */ + synchronized boolean restoreCountedOwnerSlotRefCount() { + int count = countedOwnerSlotCount(); + if (count == 0) return false; + refCount = count; + return true; + } + + /** Restore from every surviving counted slot after verifying its current storage cell. */ + boolean restoreVerifiedCurrentCountedOwnerSlotRefCount() { + int count = 0; + for (PerlOwnerSlot slot : ownerSlotSnapshot()) { + if (!slot.isActive()) continue; + RuntimeScalar owner = slot.countedScalarOwner(); + if (owner == null) continue; + PerlOwnerSlot.Kind kind = slot.kind(); + if ((kind == PerlOwnerSlot.Kind.ARRAY_ELEMENT + || kind == PerlOwnerSlot.Kind.HASH_ELEMENT) + && owner.isCurrentAggregateElementOwner(this, slot)) { + count++; + } else if ((kind == PerlOwnerSlot.Kind.SCALAR_CELL + || kind == PerlOwnerSlot.Kind.CODE_SLOT + || kind == PerlOwnerSlot.Kind.ARGUMENT_VALUE + || kind == PerlOwnerSlot.Kind.RETURN_VALUE + || kind == PerlOwnerSlot.Kind.TIE_HANDLER) + && owner.isCurrentCountedScalarOwner(this, slot)) { + count++; + } + } + if (count == 0) return false; + refCount = count; + return true; + } + /** Number of distinct captured pad slots that own this referent. */ public int semanticCaptureOwnerCount() { return ownerSlotCount(PerlOwnerSlot.Kind.CLOSURE_PAD); @@ -360,6 +609,16 @@ public synchronized int nativeCaptureOwnerCount() { return count; } + /** Count all slots whose owners are represented directly by the owner ledger. */ + public synchronized int nativeOwnerSlotCount() { + if (ownerSlots == null) return 0; + int count = 0; + for (PerlOwnerSlot slot : ownerSlots) { + if (slot.isNativeOwner()) count++; + } + return count; + } + // Conservative gate for the tied-handler reachability fallback. Once a // base has appeared in a tie handler's strong object graph it remains // marked; a stale true only permits the exact walker to run, while false @@ -416,12 +675,14 @@ public void activateOwnerTracking() { } public void recordActiveOwner(RuntimeScalar scalar) { + scalar.recordCountedOwner(this); if (activeOwners != null) { activeOwners.add(scalar); } } public void releaseActiveOwner(RuntimeScalar scalar) { + scalar.releaseCountedOwner(this); if (activeOwners != null) { activeOwners.remove(scalar); } @@ -466,15 +727,35 @@ public synchronized java.util.Set ownerSlotSnapshot() { return snapshot; } + private synchronized java.util.List closureCaptureOwnerSnapshot() { + if (ownerSlots == null || ownerSlots.isEmpty()) return java.util.List.of(); + java.util.ArrayList snapshot = new java.util.ArrayList<>(); + for (PerlOwnerSlot slot : ownerSlots) { + PerlOwnerSlot.Kind kind = slot.kind(); + if (kind == PerlOwnerSlot.Kind.SCALAR_CAPTURE + || kind == PerlOwnerSlot.Kind.CLOSURE_PAD) { + snapshot.add(slot); + } + } + return snapshot.isEmpty() ? java.util.List.of() : snapshot; + } + + /** Release closure pads whose CODE owners have no surviving owner identity. */ + void releaseUnownedClosureCaptures() { + for (PerlOwnerSlot ownerSlot : closureCaptureOwnerSnapshot()) { + if (!ownerSlot.isActive()) continue; + RuntimeCode captureOwner = ownerSlot.captureOwner(); + if (captureOwner != null + && !captureOwner.hasPerlOwnerForCaptureOtherThan(null)) { + captureOwner.releaseCaptures(); + } + } + } + /** - * D-W6.14: count owners that are reachable from package globals or - * live my-vars. This is the strict version used by the production - * rescue at MortalList.flush refCount→0: only objects whose owners - * can be reached from real roots are kept alive. Phantom owners - * (scalars that should be dead but haven't been released yet) are - * excluded. Cycles with no external root → all owners unreachable - * → 0 → DESTROY fires (matching real Perl's cycle leak behavior - * resolved by user weaken()). + * Count owners that are reachable from package globals or live my-vars. + * This compatibility fallback remains for ownership sources that have not + * yet migrated to explicit slot identities. */ public int reachableOwnerCount() { if (activeOwners == null) return 0; @@ -558,10 +839,11 @@ private static final class OwnerTrace { } /** - * Immutable trace record retained between queueing and draining an owned - * decrement. It deliberately does not retain the scalar itself: source - * identity and acquisition site are enough for shutdown diagnosis, and a - * trace facility must not change Perl/JVM reachability. + * Release token retained between queueing and draining an owned decrement. + * It deliberately does not retain the scalar itself: source identity and + * acquisition site are enough for shutdown diagnosis, and a trace facility + * must not change Perl/JVM reachability. Completion is idempotent so the + * pending-release count and owner-slot token retire exactly once. */ static final class PendingOwnerRelease { final int scalarIdentity; @@ -569,6 +851,8 @@ static final class PendingOwnerRelease { final String acquireSite; final String queueSite; final PerlOwnerSlot.PendingRelease ownerSlotRelease; + final java.util.concurrent.atomic.AtomicBoolean completed = + new java.util.concurrent.atomic.AtomicBoolean(); PendingOwnerRelease(int scalarIdentity, long referentGeneration, String acquireSite, String queueSite) { @@ -622,14 +906,24 @@ public synchronized void releaseOwner(RuntimeScalar owner, String site) { } /** Move an active owner trace into the deferred-decrement queue. */ - public synchronized PendingOwnerRelease queueOwnerRelease(RuntimeScalar owner, String queueSite) { - if (!refCountTrace || !REFCOUNT_TRACE_ENV) return null; + public PendingOwnerRelease queueOwnerRelease(RuntimeScalar owner, String queueSite) { + PerlOwnerSlot.PendingRelease slotRelease = owner.deferCountedOwnerRelease(this); + registerPendingOwnerRelease(slotRelease); + if (!refCountTrace || !REFCOUNT_TRACE_ENV) { + return new PendingOwnerRelease( + System.identityHashCode(owner), 0, "counted owner-slot", queueSite, slotRelease); + } + return queueTracedOwnerRelease(owner, queueSite, slotRelease); + } + + private synchronized PendingOwnerRelease queueTracedOwnerRelease( + RuntimeScalar owner, String queueSite, PerlOwnerSlot.PendingRelease slotRelease) { int scalarIdentity = System.identityHashCode(owner); java.util.LinkedHashMap owners = traceOwners.get(this); OwnerTrace ownerTrace = owners == null ? null : owners.remove(scalarIdentity); String acquireSite = ownerTrace == null ? "" : ownerTrace.acquireSite; PendingOwnerRelease release = new PendingOwnerRelease( - scalarIdentity, traceReferentGeneration(this), acquireSite, queueSite); + scalarIdentity, traceReferentGeneration(this), acquireSite, queueSite, slotRelease); pendingTraceOwnerReleases .computeIfAbsent(this, k -> new java.util.ArrayList<>()) .add(release); @@ -639,6 +933,7 @@ public synchronized PendingOwnerRelease queueOwnerRelease(RuntimeScalar owner, S /** Queue provenance for a deferred owner-slot decrement. */ synchronized PendingOwnerRelease queueOwnerSlotRelease( PerlOwnerSlot.PendingRelease ownerSlotRelease) { + registerPendingOwnerRelease(ownerSlotRelease); PendingOwnerRelease release = new PendingOwnerRelease( 0, REFCOUNT_TRACE_ENV ? traceReferentGeneration(this) : 0, "owner-slot", "MortalList.deferOwnerSlotDecrement", ownerSlotRelease); @@ -652,7 +947,14 @@ synchronized PendingOwnerRelease queueOwnerSlotRelease( /** Mark the exact queued trace record as drained without relying on scalar state. */ public synchronized void completeQueuedOwnerRelease(PendingOwnerRelease release, String releaseSite) { if (release == null) return; - if (release.ownerSlotRelease != null) release.ownerSlotRelease.complete(); + if (!release.completed.compareAndSet(false, true)) return; + if (release.ownerSlotRelease != null) { + if (release.ownerSlotRelease.kind() == PerlOwnerSlot.Kind.RETURN_VALUE) { + pendingReturnOwners--; + } + release.ownerSlotRelease.complete(); + } + pendingOwnerReleases--; if (!REFCOUNT_TRACE_ENV) return; java.util.ArrayList releases = pendingTraceOwnerReleases.get(this); if (releases != null) { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 9d02b3bef6..cca2903630 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -79,15 +79,19 @@ private record SortComparatorFrame(RuntimeCode comparator, int entryDepth) {} public static final class CallArguments extends RuntimeScalar { private final RuntimeBase[] values; private final java.util.ArrayList listResultOwners = new java.util.ArrayList<>(); + private final java.util.ArrayList stagedScalarOwners = new java.util.ArrayList<>(); + private final java.util.ArrayList argumentAliasOwners = + new java.util.ArrayList<>(); private int size; private final ExecutionRuntimeState ownerState; - private final java.lang.ref.WeakReference registration; + private final int argumentDepth; + private RuntimeArray materializedArguments; private CallArguments(int capacity) { values = new RuntimeBase[Math.max(0, capacity)]; ownerState = PerlRuntime.current().executionState(); - registration = new java.lang.ref.WeakReference<>(this); - ownerState.callArgumentFrames.add(registration); + argumentDepth = ownerState.argsStack.size(); + ownerState.callArgumentFrames.add(this); } public void add(RuntimeBase value) { @@ -95,13 +99,20 @@ public void add(RuntimeBase value) { throw new IllegalStateException("too many call arguments"); } values[size++] = value; - // A list-valued call result can hold a fresh object returned by - // an inline constructor. The Java staging array keeps that list - // reachable, but it is not a Perl owner and cannot protect the - // returned object's weak observers when later arguments run. - // Give strong references in staged list results ordinary scalar - // owner slots until the enclosing call has consumed its arguments. - if (size < values.length && value instanceof RuntimeList) { + // Later argument expressions and callee cleanup may drain a + // scalar return's mortal owner. Keep a separate argument owner + // through the call; persistent caller cells remain aliased borrows. + if (value instanceof RuntimeScalar scalar) { + if (hasPersistentArgumentCell(scalar)) { + argumentAliasOwners.add(scalar.retainArgumentAliasOwner()); + } + if ((scalar.type & RuntimeScalarType.REFERENCE_BIT) != 0 + && scalar.value instanceof RuntimeBase + && !WeakRefRegistry.isweak(scalar) + && scalar.isMortalReturnSnapshot()) { + stagedScalarOwners.add(RuntimeScalar.retainStagedArgumentValue(scalar)); + } + } else if (size < values.length && value instanceof RuntimeList) { retainListResultOwners(value); } } @@ -111,43 +122,21 @@ private void retainListResultOwners(RuntimeBase value) { for (RuntimeBase element : list.elements) { retainListResultOwners(element); } - } else if (value instanceof RuntimeScalar scalar - && scalar.value instanceof RuntimeBase base - && base.blessId != 0 - && WeakRefRegistry.hasWeakRefsTo(base) - && !WeakRefRegistry.isweak(scalar)) { - RuntimeScalar owner = new RuntimeScalar(); - owner.set(scalar); - listResultOwners.add(owner); - } - } - - private boolean containsReferent(RuntimeBase referent) { - for (int i = 0; i < size; i++) { - RuntimeBase value = values[i]; - if (argumentValueOwnsReferent(value, referent)) return true; - } - return false; - } - - private boolean argumentValueOwnsReferent(RuntimeBase value, RuntimeBase referent) { - if (value instanceof RuntimeScalar scalar) { - return scalar.value == referent && !WeakRefRegistry.isweak(scalar); - } - if (value instanceof RuntimeList list) { - for (RuntimeBase element : list.elements) { - if (argumentValueOwnsReferent(element, referent)) return true; - } } else if (value instanceof RuntimeArray array) { for (RuntimeScalar element : array.elements) { - if (argumentValueOwnsReferent(element, referent)) return true; + retainListResultOwners(element); } } else if (value instanceof RuntimeHash hash) { for (RuntimeScalar element : hash.elements.values()) { - if (argumentValueOwnsReferent(element, referent)) return true; + retainListResultOwners(element); } + } else if (value instanceof RuntimeScalar scalar + && scalar.value instanceof RuntimeBase + && (scalar.type & RuntimeScalarType.REFERENCE_BIT) != 0 + && !WeakRefRegistry.isweak(scalar)) { + RuntimeScalar owner = RuntimeScalar.retainStagedArgumentValue(scalar); + listResultOwners.add(owner); } - return false; } private RuntimeBase[] argumentValues() { @@ -155,7 +144,9 @@ private RuntimeBase[] argumentValues() { } public RuntimeArray toRuntimeArray() { + if (materializedArguments != null) return materializedArguments; RuntimeArray args = new RuntimeArray(size); + materializedArguments = args; for (RuntimeBase value : argumentValues()) { value.setArrayOfAlias(args); } @@ -163,34 +154,51 @@ public RuntimeArray toRuntimeArray() { } public void finish() { - ownerState.callArgumentFrames.remove(registration); + ownerState.callArgumentFrames.remove(this); + if (materializedArguments != null) { + RuntimeArray arguments = materializedArguments; + materializedArguments = null; + if (arguments.ownedAliasElements != null) { + for (RuntimeScalar scalar : arguments.ownedAliasElements + .toArray(new RuntimeScalar[0])) { + PerlOwnerSlot owner = scalar.countedOwnerSlot(); + if (owner != null && owner.isActive() + && owner.kind() == PerlOwnerSlot.Kind.ARGUMENT_VALUE) { + RuntimeScalar.scopeExitCleanup(scalar); + } + arguments.forgetOwnedAliasElement(scalar); + } + } + } for (RuntimeScalar owner : listResultOwners) { - owner.set((RuntimeScalar) null); + RuntimeScalar.releaseStagedArgumentValue(owner); } listResultOwners.clear(); + for (RuntimeScalar owner : stagedScalarOwners) { + RuntimeScalar.releaseStagedArgumentValue(owner); + } + stagedScalarOwners.clear(); + for (RuntimeScalar.ArgumentAliasOwner owner : argumentAliasOwners) { + owner.release(); + } + argumentAliasOwners.clear(); MortalList.drainDeferredArgumentOwnerReleases(); } - } - static boolean hasActiveArgumentOwner(RuntimeScalar scalar) { - if (scalar == null || !(scalar.value instanceof RuntimeBase referent)) return false; - ExecutionRuntimeState state = PerlRuntime.current().executionState(); - for (RuntimeArray args : state.argsStack) { - for (RuntimeScalar element : args.elements) { - if (element != null && element.value == referent - && !WeakRefRegistry.isweak(element)) return true; - } - } - for (int i = state.callArgumentFrames.size() - 1; i >= 0; i--) { - java.lang.ref.WeakReference reference = state.callArgumentFrames.get(i); - CallArguments frame = reference.get(); - if (frame == null) { - state.callArgumentFrames.remove(i); - } else if (frame.containsReferent(referent)) { - return true; + /** Release a partially evaluated call that will never enter its callee. */ + public Throwable abandon(Throwable failure) { + finish(); + // A non-local return carries a result whose mortal owner must + // survive until the receiving call boundary consumes it. + if (!(failure instanceof PerlNonLocalReturnException)) { + MortalList.flushAboveMark(); } + return failure; } - return false; + } + + private static boolean hasPersistentArgumentCell(RuntimeScalar scalar) { + return scalar.hasPersistentArgumentStorageCell() || isCurrentArgumentAlias(scalar); } public static CallArguments newCallArguments(int capacity) { @@ -774,6 +782,17 @@ private static Deque argsStack() { * Java thread. Weak CODE backrefs, especially Sub::Defer's self slot, must * not be cleared while the referent CODE is still running. */ + /** An executing frame retains each of its explicit scalar capture slots. */ + static boolean activeCodeOwnsCapture(RuntimeScalar scalar) { + for (RuntimeCode code : activeCodeStack()) { + if (code.capturedScalarOwnerSlots == null) continue; + for (PerlOwnerSlot slot : code.capturedScalarOwnerSlots) { + if (slot != null && slot.isActive() && slot.referent() == scalar) return true; + } + } + return false; + } + private static Deque activeCodeStack() { return PerlRuntime.current().executionState().activeCodeStack; } @@ -783,16 +802,16 @@ private static Deque activeCodeStack(ExecutionRuntimeState executio } private static final class ActiveLexicalFrame { + private final PerlOwnerSlot codeOwner = new PerlOwnerSlot(PerlOwnerSlot.Kind.ACTIVE_CODE); private RuntimeCode code; private Map cells; + private Map aggregateOwnerSlots; private String previousCallerPackage; private String callSitePackage; private ActiveLexicalFrame(RuntimeCode code, String previousCallerPackage, String callSitePackage) { - this.code = code; - this.previousCallerPackage = previousCallerPackage; - this.callSitePackage = callSitePackage; + rebind(code, previousCallerPackage, callSitePackage); } private RuntimeCode code() { @@ -804,6 +823,7 @@ private void rebind(RuntimeCode code, String previousCallerPackage, this.code = code; this.previousCallerPackage = previousCallerPackage; this.callSitePackage = callSitePackage; + codeOwner.acquireActiveCodeIdentity(code); } private String previousCallerPackage() { @@ -815,6 +835,14 @@ private String callSitePackage() { } private void clearForReuse() { + codeOwner.release(); + if (aggregateOwnerSlots != null) { + for (PerlOwnerSlot ownerSlot : aggregateOwnerSlots.values()) { + if (ownerSlot != null) ownerSlot.release(); + } + aggregateOwnerSlots.clear(); + aggregateOwnerSlots = null; + } code = null; cells = null; previousCallerPackage = null; @@ -827,11 +855,40 @@ private Map cells() { private void put(String name, RuntimeBase cell) { if (cells == null) cells = new HashMap<>(); - cells.put(name, cell); + RuntimeBase previous = cells.put(name, cell); + if (previous == cell) return; + releaseAggregateOwner(name); + if ((cell instanceof RuntimeArray || cell instanceof RuntimeHash) + && MyVarCleanupStack.isRegistered(cell)) { + if (aggregateOwnerSlots == null) aggregateOwnerSlots = new HashMap<>(); + PerlOwnerSlot ownerSlot = new PerlOwnerSlot(PerlOwnerSlot.Kind.ACTIVE_LEXICAL); + ownerSlot.acquireActiveLexicalIdentity(cell); + aggregateOwnerSlots.put(name, ownerSlot); + } } private void remove(String name) { if (cells != null) cells.remove(name); + releaseAggregateOwner(name); + } + + private void releaseAggregateOwner(String name) { + if (aggregateOwnerSlots == null) return; + PerlOwnerSlot ownerSlot = aggregateOwnerSlots.remove(name); + if (ownerSlot != null) ownerSlot.release(); + if (aggregateOwnerSlots.isEmpty()) aggregateOwnerSlots = null; + } + + private int removeAggregateCell(RuntimeBase aggregate) { + if (cells == null || aggregateOwnerSlots == null) return 0; + for (Map.Entry entry : new ArrayList<>(cells.entrySet())) { + if (entry.getValue() == aggregate + && aggregateOwnerSlots.containsKey(entry.getKey())) { + remove(entry.getKey()); + return 1; + } + } + return 0; } } @SuppressWarnings("unchecked") @@ -1068,6 +1125,7 @@ private static void popActiveCode( if (frame.code() == code) { iterator.remove(); recycleActiveLexicalFrame(frame, executionState); + break; } } } @@ -1077,6 +1135,7 @@ private static void popActiveCode( if (!executionState.activeCodeCallSites.isEmpty()) { executionState.activeCodeCallSites.pop(); } + code.finishActiveCodeOwnerRelease(); return; } int index = 0; @@ -1084,6 +1143,7 @@ private static void popActiveCode( if (iterator.next() == code) { iterator.remove(); removeActiveCodeCallSiteAt(executionState, index); + code.finishActiveCodeOwnerRelease(); break; } index++; @@ -1119,6 +1179,139 @@ public static boolean isActiveCode(RuntimeCode code) { return false; } + private boolean finalReleaseDeferredToActiveCode; + + boolean hasActiveCodeOwner() { + return ownerSlotCount(PerlOwnerSlot.Kind.ACTIVE_CODE) != 0; + } + + /** + * Whether this CODE object still has a Perl owner that can use its pad. + * Closure-capture cleanup uses this ledger instead of proving liveness by + * walking the Java object graph. + */ + boolean hasPerlOwnerForCapture() { + return refCount > 0 + || stashRefCount > 0 + || localBindingExists + || hasActiveCodeOwner() + || hasCallTransferOwner() + || hasOwnerSlots() + || isActiveCode(this) + || isConstantCv + || constantValue != null + || GlobalVariable.installedGlobalCodeRefsView().contains(this) + || GlobalVariable.hasLocalizedCodeRef(this); + } + + /** Release an untracked CV's pad tree when its last Perl owner leaves. */ + void releasePadConstantsForUntrackedOwnerSlot() { + // The compatibility count can outlive the scalar slot that contributed + // it. Once a slot release reaches this point, capture liveness comes + // from the remaining owner identities and non-scalar CODE roots. + if (PerlRuntime.currentOrNull() == null + || hasPerlOwnerForCaptureOtherThan(null) + || padConstantOwnerSlots.isEmpty()) { + return; + } + clearPadConstantWeakRefs(); + } + + /** Whether a Perl owner other than one scalar slot still needs this CV's captures. */ + public boolean hasPerlOwnerForCaptureOtherThan(PerlOwnerSlot excludedOwner) { + // Do not use refCount here: this release path handles CODE objects + // whose compatibility count can remain positive after their last + // real scalar owner has gone away. + return stashRefCount > 0 + || localBindingExists + || hasActiveCodeOwner() + || hasCallTransferOwner() + || hasOwnerSlotsOtherThan(excludedOwner) + || isActiveCode(this) + || isConstantCv + || constantValue != null + || GlobalVariable.installedGlobalCodeRefsView().contains(this) + || GlobalVariable.hasLocalizedCodeRef(this); + } + + /** A removed stash slot requests final state cleanup at the last owner boundary. */ + void noteStashOwnerReleased() { + if (perlLifecycleState() == RuntimeBase.PerlLifecycleState.DESTROYED + || stashRefCount > 0 + || GlobalVariable.installedGlobalCodeRefsView().contains(this)) return; + deferFinalReleaseToActiveCode(); + finishActiveCodeOwnerRelease(); + } + + /** + * Retire frame-temporary scalar cells that are the only direct holders of + * this generated CV. The owner-slot back-reference is weak so ownership + * metadata does not extend the scalar's lifetime. + */ + public boolean releaseTemporaryCodeScalarOwners() { + if (isActiveCode(this)) return false; + boolean released = false; + for (PerlOwnerSlot ownerSlot : ownerSlotSnapshot()) { + if (!ownerSlot.isActive() + || ownerSlot.kind() != PerlOwnerSlot.Kind.SCALAR_CELL + || ownerSlot.referent() != this) continue; + RuntimeScalar owner = ownerSlot.countedScalarOwner(); + RuntimeArray ownedArgumentArray = owner != null + && owner.containerOwner instanceof RuntimeArray array + && array.elementsAliased && array.ownsElement(owner) + ? array : null; + RuntimeArray borrowedArgumentArray = owner != null + && owner.containerOwner instanceof RuntimeArray array + && array.elementsAliased && !array.ownsElement(owner) + && array.elements.contains(owner) + ? array : null; + if (owner == null || owner.countedOwnerSlot() != ownerSlot + || owner.type != RuntimeScalarType.CODE || owner.value != this + || !owner.refCountOwned || owner.scopeExited + || owner.hasCodeSlotBinding() + || (owner.containerOwner != null && ownedArgumentArray == null + && borrowedArgumentArray == null) + || owner.localBindingExists || owner.isPackageGlobalRoot + || MyVarCleanupStack.isRegistered(owner)) continue; + RuntimeScalar.scopeExitCleanup(owner); + if (ownedArgumentArray != null) ownedArgumentArray.forgetOwnedAliasElement(owner); + released = true; + } + return released; + } + + /** Rebind shared capture identities when this wrapper becomes the Perl CV. */ + void rebindCaptureOwnerSlots() { + if (capturedScalarOwnerSlots != null) { + for (PerlOwnerSlot ownerSlot : capturedScalarOwnerSlots) { + if (ownerSlot != null) ownerSlot.rebindCaptureOwner(this); + } + } + if (capturedAggregateOwnerSlots != null) { + for (PerlOwnerSlot ownerSlot : capturedAggregateOwnerSlots) { + if (ownerSlot != null) ownerSlot.rebindCaptureOwner(this); + } + } + } + + void deferFinalReleaseToActiveCode() { + finalReleaseDeferredToActiveCode = true; + if (refCount == Integer.MIN_VALUE) refCount = 0; + } + + void finishActiveCodeOwnerRelease() { + if (!finalReleaseDeferredToActiveCode || hasActiveCodeOwner() || isActiveCode(this)) return; + // CODE can reach refCount zero while a transient call-frame, argument, + // or END-block owner still protects it. Recheck at each such owner + // release boundary; otherwise the count can stay at zero after its + // last real owner leaves and the closure captures never get released. + if (hasPerlOwnerForCaptureOtherThan(null) + || activeEndBlockExecutions > 0 || SpecialBlock.hasPendingEndBlock(this)) return; + finalReleaseDeferredToActiveCode = false; + refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroyFromCountedOwner(this); + } + public static RuntimeCode getActiveCodeAt(int depth) { if (depth < 0) return null; int i = 0; @@ -1392,10 +1585,46 @@ public static void registerCurrentActiveLexical(String variableName, RuntimeBase if (!frames.isEmpty() && variableName != null && cell != null) { frames.peek().put(variableName, cell); } else if (variableName != null && cell != null) { - state.topLevelLexicals.put(variableName, cell); + putTopLevelLexical(state, variableName, cell); + } + } + + private static void putTopLevelLexical( + ExecutionRuntimeState state, String variableName, RuntimeBase cell) { + RuntimeBase previous = state.topLevelLexicals.put(variableName, cell); + if (previous == cell) return; + PerlOwnerSlot previousSlot = state.topLevelLexicalOwnerSlots.remove(variableName); + if (previousSlot != null) previousSlot.release(); + if ((cell instanceof RuntimeArray || cell instanceof RuntimeHash) + && MyVarCleanupStack.isRegistered(cell)) { + PerlOwnerSlot ownerSlot = new PerlOwnerSlot(PerlOwnerSlot.Kind.ACTIVE_LEXICAL); + ownerSlot.acquireActiveLexicalIdentity(cell); + state.topLevelLexicalOwnerSlots.put(variableName, ownerSlot); } } + /** Release pad-map owner identities when an aggregate lexical leaves scope. */ + static boolean releaseActiveLexicalAggregateOwner(RuntimeBase aggregate) { + if (!(aggregate instanceof RuntimeArray || aggregate instanceof RuntimeHash) + || PerlRuntime.currentOrNull() == null) return false; + ExecutionRuntimeState state = PerlRuntime.current().executionState(); + Deque frames = activeLexicalFrames(state); + ActiveLexicalFrame currentFrame = frames.peek(); + if (currentFrame != null) return currentFrame.removeAggregateCell(aggregate) != 0; + for (Map.Entry entry + : new ArrayList<>(state.topLevelLexicals.entrySet())) { + if (entry.getValue() != aggregate) continue; + state.topLevelLexicals.remove(entry.getKey()); + PerlOwnerSlot ownerSlot = state.topLevelLexicalOwnerSlots.remove(entry.getKey()); + if (ownerSlot != null) { + ownerSlot.release(); + return true; + } + return false; + } + return false; + } + /** * Select eval STRING captures for Perl's package-DB rule. An eval run by * a DB subroutine is evaluated in the lexical pad of the code being @@ -1465,6 +1694,7 @@ private static void pushArgs( RuntimeArray args, boolean snapshotRequired, ExecutionRuntimeState executionState) { executionState.argsStack.push(args); + executionState.argumentAliasOwnerFrames.push(retainArgumentAliasOwners(args)); // Snapshot the args list so @DB::args stays pristine even if the sub // later shifts/pops from @_. if (args == null || args.elements.isEmpty()) { @@ -1476,6 +1706,42 @@ private static void pushArgs( } } + private static List retainArgumentAliasOwners( + RuntimeArray args) { + ArrayList owners = new ArrayList<>(); + if (args == null) return owners; + for (RuntimeScalar scalar : args.elements) { + if (scalar == null || !hasPersistentArgumentCell(scalar)) continue; + owners.add(scalar.retainArgumentAliasOwner()); + } + return owners; + } + + /** Retire the active {@code @_} alias identity for one removed argument slot. */ + static void argumentArraySlotRemoved(RuntimeArray args, RuntimeScalar scalar) { + if (args == null || scalar == null) return; + PerlRuntime runtime = PerlRuntime.currentOrNull(); + if (runtime == null) return; + ExecutionRuntimeState state = runtime.executionState(); + Iterator argumentFrames = state.argsStack.iterator(); + Iterator> ownerFrames = + state.argumentAliasOwnerFrames.iterator(); + while (argumentFrames.hasNext() && ownerFrames.hasNext()) { + RuntimeArray activeArgs = argumentFrames.next(); + List owners = ownerFrames.next(); + if (activeArgs != args) continue; + for (Iterator iterator = owners.iterator(); + iterator.hasNext();) { + RuntimeScalar.ArgumentAliasOwner owner = iterator.next(); + if (owner.matches(scalar)) { + iterator.remove(); + owner.release(); + return; + } + } + } + } + public static void pushCallContext(int callContext) { pushCallContext(callContext, PerlRuntime.current().executionState()); } @@ -1506,6 +1772,22 @@ private static void popArgs(ExecutionRuntimeState executionState) { RuntimeArray exitedArgs = null; if (!stack.isEmpty()) { exitedArgs = stack.pop(); + if (!executionState.argumentAliasOwnerFrames.isEmpty()) { + for (RuntimeScalar.ArgumentAliasOwner owner : + executionState.argumentAliasOwnerFrames.pop()) { + owner.release(); + } + } + } + // A return inside an argument expression can bypass the invocation + // wrapper without throwing. Retire staging frames belonging to this + // callee, preserving frames created in its still-active callers. + if (!executionState.callArgumentFrames.isEmpty()) { + for (CallArguments frame : executionState.callArgumentFrames.toArray(new CallArguments[0])) { + if (frame.argumentDepth > stack.size()) { + frame.finish(); + } + } } Deque> pStack = executionState.pristineArgsStack; if (!pStack.isEmpty()) { @@ -1539,20 +1821,6 @@ private static void cleanupOwnedArgumentAliases(RuntimeArray args) { RuntimeScalar[] owned = args.ownedAliasElements.toArray(new RuntimeScalar[0]); for (RuntimeScalar scalar : owned) { RuntimeScalar.scopeExitCleanup(scalar); - if (scalar.type == RuntimeScalarType.CODE - && scalar.value instanceof RuntimeCode code - && (code.capturedScalars != null || code.capturedAggregates != null) - && code.refCount > 0 - && !isActiveCode(code) - && !ReachabilityWalker.isReachableFromRoots(code)) { - // A closure may have reached @_ after its lexical scope had - // already ended. In that case the scalar's original cleanup - // ran while it still appeared to be held by an alias array, - // leaving a positive manual CV count. Once this argument - // frame is gone, release its captures only if no Perl root - // can still reach the CV. - code.releaseCaptures(); - } args.forgetOwnedAliasElement(scalar); } } @@ -1602,6 +1870,13 @@ static Object currentArgumentAliasFrame(RuntimeScalar scalar) { return null; } + /** Resolve the explicit source frame associated with a queued scalar owner. */ + static Object argumentAliasFrameForOwner(RuntimeScalar scalar) { + if (scalar == null || PerlRuntime.currentOrNull() == null) return null; + Object activeFrame = currentArgumentAliasFrame(scalar); + return activeFrame != null ? activeFrame : scalar.copiedFromArgumentFrameToken(); + } + /** True only while the argument frame represented by {@code token} is active. */ static boolean isArgumentFrameActive(Object token) { if (token == null || PerlRuntime.currentOrNull() == null) return false; @@ -1853,6 +2128,90 @@ public static RuntimeList returnList(RuntimeBase retVal, int callContext) { return returnList(retVal, callContext, true); } + /** Materialize context-sensitive aggregate values before their declaring scope ends. */ + public static RuntimeBase prepareScopeResult(RuntimeBase result, int context) { + if (context == RuntimeContextType.LVALUE || context == RuntimeContextType.LVALUE_LIST + || context == RuntimeContextType.OBJECT) { + retainTiedScopeResultOwners(result); + } else if (context != RuntimeContextType.VOID && result instanceof RuntimeScalar scalar + && scalar.type == RuntimeScalarType.TIED_SCALAR) { + // FETCH belongs to this rvalue evaluation, before its lexical magic + // can retire. A returned lvalue instead retains the magic's storage. + result = scalar.tiedFetch(); + } + if (result instanceof RuntimeArray || result instanceof RuntimeHash) { + if (context == RuntimeContextType.LIST) { + result = result.getList(); + } else if (context == RuntimeContextType.SCALAR || context == RuntimeContextType.OBJECT) { + result = result.scalar(); + } + } + return prepareScopeReturn(result, context, true); + } + + private static void retainTiedScopeResultOwners(RuntimeBase result) { + if (result instanceof RuntimeList list) { + for (RuntimeBase value : list.elements) retainTiedScopeResultOwners(value); + return; + } + TiedVariableBase magic = result instanceof TiedVariableBase tied ? tied + : result instanceof RuntimeScalar scalar + && scalar.type == RuntimeScalarType.TIED_SCALAR + && scalar.value instanceof TiedVariableBase tied ? tied : null; + if (magic != null) { + RuntimeScalar storage = magic.storageOwnerReference(); + if (storage != null) RuntimeScalar.copyToMortalReturnSlot(storage); + } + } + + /** Acquire rvalue reference owners before lexical cleanup can retire their referents. */ + public static RuntimeBase prepareScopeReturn( + RuntimeBase returned, int callContext, boolean copyScalars) { + if (!copyScalars || callContext == RuntimeContextType.VOID + || callContext == RuntimeContextType.LVALUE + || callContext == RuntimeContextType.LVALUE_LIST) return returned; + // A CODE object can reach this path directly from a `do` block or + // conditional expression without first passing through a RuntimeScalar. + // Acquire the return owner before the expression's lexical scope exits. + if (returned instanceof RuntimeCode code) { + return RuntimeScalar.copyToMortalReturnSlot(new RuntimeScalar(code)); + } + if (returned instanceof RuntimeScalar scalar + && (scalar.type & RuntimeScalarType.REFERENCE_BIT) != 0 + && !scalar.isMortalReturnSnapshot()) { + return RuntimeScalar.copyToMortalReturnSlot(scalar); + } + if (returned instanceof RuntimeList list && !(returned instanceof RuntimeControlFlowList)) { + for (RuntimeBase value : list.elements) { + if (value instanceof RuntimeScalar scalar + && (scalar.type & RuntimeScalarType.REFERENCE_BIT) != 0 + && !scalar.isMortalReturnSnapshot()) { + return list.copyToMortalReturnSlots(); + } + } + } + return returned; + } + + /** Materialize a local array's rvalue return before its element slots end. */ + public static RuntimeBase prepareLocalArrayReturn( + RuntimeBase returned, RuntimeArray localArray, int callContext, boolean copyScalars) { + if (localArray == null || returned != localArray || !copyScalars + || callContext == RuntimeContextType.LVALUE + || callContext == RuntimeContextType.LVALUE_LIST) return returned; + if (callContext == RuntimeContextType.VOID) return new RuntimeList(); + if (callContext == RuntimeContextType.SCALAR || callContext == RuntimeContextType.OBJECT) { + return localArray.scalar(); + } + RuntimeList result = new RuntimeList(); + for (int i = 0, size = localArray.size(); i < size; i++) { + RuntimeScalar value = localArray.get(i); + result.elements.add(value == null ? new RuntimeScalar() + : RuntimeScalar.copyToMortalReturnSlot(value)); + } + return result; + } + /** Return the scalar lvalue represented by {@code keys %hash}. */ public static RuntimeScalar keysLvalue(RuntimeBase container) { RuntimeScalar result = new RuntimeScalar(container.keys(RuntimeContextType.SCALAR)); @@ -2102,11 +2461,66 @@ public static RuntimeList coerceScalarCallResult(RuntimeList result, int effecti } } + /** Consume a map/grep return at an eval boundary after releasing its lexical owners. */ + public static RuntimeList finishEvalCallbackReturn( + PerlNonLocalReturnException transfer, Object[] locals, int context) { + return finishEvalCallbackReturn(transfer, locals, context, () -> {}); + } + + /** Keep the return owners through backend-specific argument and local restoration. */ + public static RuntimeList finishEvalCallbackReturn( + PerlNonLocalReturnException transfer, Object[] locals, int context, Runnable unwind) { + java.util.ArrayList owners = new java.util.ArrayList<>(); + RuntimeBase value = retainEvalReturnReferences(transfer.returnValue, owners); + try { + // The return slots above remain counted during nested DESTROY and + // flushes. Only expression values are traversed, never referent graphs. + for (int i = locals.length - 1; i >= 0; i--) { + MortalList.evalExceptionScopeCleanup(locals[i]); + MyVarCleanupStack.unregister(locals[i]); + } + unwind.run(); + MortalList.flush(); + GlobalVariable.setGlobalVariable("main::@", ""); + return coerceScalarCallResult(value == null ? new RuntimeList() : value.getList(), + context, context, true); + } finally { + for (RuntimeScalar owner : owners) { + RuntimeScalar.releaseStagedArgumentValue(owner); + } + } + } + + private static RuntimeBase retainEvalReturnReferences( + RuntimeBase value, java.util.List owners) { + if (value instanceof RuntimeList list) { + RuntimeList copy = new RuntimeList(); + for (RuntimeBase element : list.elements) { + copy.elements.add(retainEvalReturnReferences(element, owners)); + } + return copy; + } + if (value instanceof RuntimeScalar scalar + && (scalar.type & RuntimeScalarType.REFERENCE_BIT) != 0) { + RuntimeScalar owner = RuntimeScalar.retainEvalReturnValue(scalar); + owners.add(owner); + return owner; + } + return value; + } + /** True when draining a callee's scope exits cannot invalidate its result. */ private static boolean containsNoReference(RuntimeList result) { + if (result instanceof RuntimeControlFlowList flow + && flow.getControlFlowType() == ControlFlowType.RETURN) { + RuntimeBase value = flow.getReturnValue(); + return value == null || containsNoReference(value.getList()); + } return result != null && result.elements.stream().noneMatch( element -> element instanceof RuntimeScalar scalar - && (scalar.type & RuntimeScalarType.REFERENCE_BIT) != 0); + && ((scalar.type & RuntimeScalarType.REFERENCE_BIT) != 0 + || scalar.type == RuntimeScalarType.TIED_SCALAR + || scalar instanceof TiedVariableBase)); } /** @@ -2119,7 +2533,9 @@ public static void flushEvalBlockCleanupForScalarResult(RuntimeBase result) { if (result instanceof RuntimeList list) { resultHasNoReferences = containsNoReference(list); } else if (result instanceof RuntimeScalar scalar) { - resultHasNoReferences = (scalar.type & RuntimeScalarType.REFERENCE_BIT) == 0; + resultHasNoReferences = (scalar.type & RuntimeScalarType.REFERENCE_BIT) == 0 + && scalar.type != RuntimeScalarType.TIED_SCALAR + && !(scalar instanceof TiedVariableBase); } else { resultHasNoReferences = true; } @@ -2140,29 +2556,21 @@ private static RuntimeList copyReturnedReferenceScalars(RuntimeList result, int } for (RuntimeBase value : result.elements) { if (value instanceof RuntimeScalar scalar - && !isCodeScalar(scalar)) { + && !scalar.isMortalReturnSnapshot()) { if (recyclableScalarResult && result.elements.size() == 1) { // Scalar-context callers discard the list wrapper after extracting this // copied rvalue. Reuse a private one-scalar input wrapper when possible; // otherwise acquire a runtime-local wrapper for the copied value. - RuntimeScalar copied = scalar.clone(); + RuntimeScalar copied = RuntimeScalar.copyToMortalReturnSlot(scalar); if (result.replaceRecyclableScalarResult(copied)) return result; return RuntimeList.acquireScalarResult(copied); } - return result.cloneScalars(); + return result.copyToMortalReturnSlots(); } } return result; } - private static boolean isCodeScalar(RuntimeScalar scalar) { - RuntimeScalar current = scalar; - while (current != null && current.type == RuntimeScalarType.READONLY_SCALAR) { - current = (RuntimeScalar) current.value; - } - return current != null && current.type == RuntimeScalarType.CODE; - } - private static RuntimeList copyReadonlyListReturns(RuntimeList result, int effectiveContext) { if (effectiveContext != RuntimeContextType.LIST) { return result; @@ -2220,51 +2628,56 @@ public static RuntimeBase acquireMethodInvocantHold(RuntimeScalar runtimeScalar) if (base.refCount < 0) { return null; } - base.traceRefCount(+1, "RuntimeCode.method invocant hold (+1)"); - base.refCount++; - base.acquireTransientTraceOwner("method invocant hold", "RuntimeCode.acquireMethodInvocantHold"); - return base; + return new MethodInvocantHold(base); } - /** Release a hold returned by {@link #acquireMethodInvocantHold(RuntimeScalar)}. */ - public static void releaseMethodInvocantHold(RuntimeBase holdBase) { - if (holdBase == null) { - return; + /** One call's invocant edge, kept separate from every other hold of the same value. */ + private static final class MethodInvocantHold extends RuntimeScalar { + private final RuntimeBase referent; + private final PerlOwnerSlot owner = new PerlOwnerSlot(PerlOwnerSlot.Kind.ARGUMENT_VALUE); + + MethodInvocantHold(RuntimeBase referent) { + this.referent = referent; + owner.acquireCountedIdentity(referent); + referent.traceRefCount(+1, "RuntimeCode.method invocant hold (+1)"); + referent.refCount++; + referent.hadCountedReference = true; + referent.acquireTransientTraceOwner("method invocant hold", "RuntimeCode.acquireMethodInvocantHold"); } - holdBase.traceRefCount(-1, "RuntimeCode.method invocant hold release (-1)"); - holdBase.releaseTransientTraceOwner("method invocant hold", - "RuntimeCode.releaseMethodInvocantHold"); - if (holdBase.refCount > 0 && holdBase.refCount != Integer.MIN_VALUE && !holdBase.currentlyDestroying) { - if (holdBase.refCount == 1) { - // Keep the invocant alive until the caller has had a chance to - // capture a method's return value. If no real owner is added, - // the deferred decrement destroys it at the statement boundary. - MortalList.deferDecrement(holdBase); - } else { - holdBase.refCount--; + + void release(boolean abandoned) { + if (!owner.isActive()) return; + PerlOwnerSlot.PendingRelease release = owner.releaseCountedIdentity(); + referent.releaseTransientTraceOwner("method invocant hold", "RuntimeCode.MethodInvocantHold.release"); + if (referent.refCount <= 0 || referent.currentlyDestroying) { + release.complete(); + return; + } + if (referent.refCount > 1) { + // Other counted owners remain. Preserve the existing immediate + // decrement without losing this call's distinct release identity. + referent.traceRefCount(-1, "RuntimeCode.method invocant hold release (-1)"); + referent.refCount--; + release.complete(); + return; } + // Normal returns need a mortal boundary so their receiver can + // acquire the result. Abandoned calls have no receiver and drain + // this exact release immediately, including within DESTROY. + int checkpoint = MortalList.pendingSize(); + MortalList.deferOwnerSlotDecrement(referent, null, release); + if (abandoned) MortalList.drainPendingSince(checkpoint); } } - /** - * Release a method-invocant hold when control flow abandoned the call that - * would have transferred the result. Unlike an ordinary post-call release, - * reaching zero here is final and must run Perl destruction immediately. - */ - public static void releaseAbandonedMethodInvocantHold(RuntimeBase holdBase) { - if (holdBase == null) { - return; - } - holdBase.traceRefCount(-1, "RuntimeCode.abandoned method invocant hold release (-1)"); - holdBase.releaseTransientTraceOwner("method invocant hold", - "RuntimeCode.releaseAbandonedMethodInvocantHold"); - if (holdBase.refCount > 0 - && holdBase.refCount != Integer.MIN_VALUE - && !holdBase.currentlyDestroying - && --holdBase.refCount == 0) { - holdBase.refCount = Integer.MIN_VALUE; - DestroyDispatch.callDestroy(holdBase); - } + /** Release a hold returned by {@link #acquireMethodInvocantHold(RuntimeScalar)}. */ + public static void releaseMethodInvocantHold(RuntimeBase hold) { + if (hold != null) ((MethodInvocantHold) hold).release(false); + } + + /** Retire an abandoned call's invocant edge without waiting for a result receiver. */ + public static void releaseAbandonedMethodInvocantHold(RuntimeBase hold) { + if (hold != null) ((MethodInvocantHold) hold).release(true); } private static boolean isReferenceToGlobInvocant(RuntimeScalar invocant) { @@ -2405,7 +2818,57 @@ private static EmitterContext getEvalContext(String evalTag) { } public static void registerPadConstants(String className, RuntimeBase[] constants) { - PerlRuntime.current().runtimeCodeState().padConstantsByClassName.put(className, constants); + registerPadConstants(className, constants, Map.of(), Map.of()); + } + + public static void registerPadConstants( + String className, + RuntimeBase[] constants, + Map embeddedConstants, + Map> embeddedDescendants) { + RuntimeCodeRuntimeState state = PerlRuntime.current().runtimeCodeState(); + state.padConstantsByClassName.put(className, + constants == null ? new RuntimeBase[0] : constants.clone()); + state.embeddedPadConstantsByClassName.put(className, copyPadConstantArrays(embeddedConstants)); + state.embeddedPadConstantDescendantsByClassName.put( + className, copyPadConstantDescendants(embeddedDescendants)); + } + + /** Read a stable optree constant by generated code class and pad index. */ + public static RuntimeScalar getPadConstant(String className, int index) { + RuntimeCode activeCode = getActiveCodeAt(0); + RuntimeBase[] constants = activeCode == null + ? null : activeCode.padConstantsForClass(className); + if (constants == null || index < 0 || index >= constants.length + || !(constants[index] instanceof RuntimeScalar)) { + RuntimeCodeRuntimeState state = PerlRuntime.current().runtimeCodeState(); + constants = state.padConstantsByClassName.get(className); + } + if (constants == null || index < 0 || index >= constants.length + || !(constants[index] instanceof RuntimeScalar scalar)) { + throw new IllegalStateException( + "missing pad constant " + index + " for " + className); + } + return scalar; + } + + private static Map copyPadConstantArrays( + Map source) { + if (source == null || source.isEmpty()) return Collections.emptyMap(); + Map copy = new LinkedHashMap<>(); + source.forEach((name, values) -> copy.put(name, + values == null ? new RuntimeBase[0] : values.clone())); + return Collections.unmodifiableMap(copy); + } + + private static Map> copyPadConstantDescendants( + Map> source) { + if (source == null || source.isEmpty()) return Collections.emptyMap(); + Map> copy = new LinkedHashMap<>(); + source.forEach((name, descendants) -> copy.put(name, + descendants == null ? Collections.emptySet() + : Collections.unmodifiableSet(new LinkedHashSet<>(descendants)))); + return Collections.unmodifiableMap(copy); } public static void registerDisabledWarnings(String className, Set categories) { @@ -2668,6 +3131,66 @@ private void exitCall(ExecutionRuntimeState executionState) { public Map stateVariable = new HashMap<>(); public Map stateArray = new HashMap<>(); public Map stateHash = new HashMap<>(); + Map stateAggregateAliasOwners = new HashMap<>(); + + void bindStateAggregateAlias(String name, RuntimeBase aggregate) { + boolean array = aggregate instanceof RuntimeArray; + String ownerKey = (array ? "@" : "%") + name; + RuntimeScalar owner = new RuntimeScalar(); + owner.set(array ? ((RuntimeArray) aggregate).createAnonymousReference() + : ((RuntimeHash) aggregate).createAnonymousReference()); + RuntimeBase previous = array ? stateArray.put(name, (RuntimeArray) aggregate) + : stateHash.put(name, (RuntimeHash) aggregate); + RuntimeScalar previousOwner = stateAggregateAliasOwners.put(ownerKey, owner); + if (previousOwner != null) { + RuntimeScalar.scopeExitCleanup(previousOwner); + } else if (previous instanceof RuntimeArray previousArray) { + MortalList.scopeExitCleanupArray(previousArray); + } else if (previous instanceof RuntimeHash previousHash) { + MortalList.scopeExitCleanupHash(previousHash); + } + } + + /** Retire this CV's persistent pad bindings at final CV destruction. */ + void releaseStateStorage() { + clearPadConstantWeakRefs(); + if (stateVariable.isEmpty() && stateArray.isEmpty() && stateHash.isEmpty() + && stateAggregateAliasOwners.isEmpty()) { + stateVariableInitialized.clear(); + return; + } + // Detach all bindings before cleanup: destructors may re-enter Perl. + // State aliases can address the same cell under more than one name. + Set bindings = Collections.newSetFromMap(new IdentityHashMap<>()); + bindings.addAll(stateVariable.values()); + for (var entry : stateArray.entrySet()) { + if (!stateAggregateAliasOwners.containsKey("@" + entry.getKey())) { + bindings.add(entry.getValue()); + } + } + for (var entry : stateHash.entrySet()) { + if (!stateAggregateAliasOwners.containsKey("%" + entry.getKey())) { + bindings.add(entry.getValue()); + } + } + // A refalias owns a reference, rather than the source aggregate's + // lexical binding. Retiring it must leave the caller's binding intact. + bindings.addAll(stateAggregateAliasOwners.values()); + stateVariable.clear(); + stateArray.clear(); + stateHash.clear(); + stateAggregateAliasOwners.clear(); + stateVariableInitialized.clear(); + for (RuntimeBase binding : bindings) { + if (binding instanceof RuntimeScalar scalar) { + RuntimeScalar.scopeExitCleanup(scalar); + } else if (binding instanceof RuntimeArray array) { + MortalList.scopeExitCleanupArray(array); + } else if (binding instanceof RuntimeHash hash) { + MortalList.scopeExitCleanupHash(hash); + } + } + } public RuntimeList constantValue; // Field to hold the thread compiling this code public Supplier compilerSupplier; @@ -2730,6 +3253,9 @@ void restoreClonedSelfReference(RuntimeScalar selfRef) { */ public RuntimeScalar[] capturedScalars; + /** One owner identity for each captured scalar pad cell. */ + public PerlOwnerSlot[] capturedScalarOwnerSlots; + /** Generated closure fields were copied into capturedScalars/aggregates. */ public boolean captureFieldsRecorded; @@ -2870,6 +3396,9 @@ public void unbindActiveLexical(String variableName, RuntimeBase cell) { } if (runtime.executionState().topLevelLexicals.get(variableName) == cell) { runtime.executionState().topLevelLexicals.remove(variableName); + PerlOwnerSlot ownerSlot = runtime.executionState().topLevelLexicalOwnerSlots + .remove(variableName); + if (ownerSlot != null) ownerSlot.release(); } } @@ -2948,6 +3477,11 @@ public void setLexicalAlias(String variableName, RuntimeBase replacement) { * cascade to clear weak references (e.g., in Sub::Defer's %DEFERRED hash). */ public int stashRefCount = 0; + /** All GP/CODE edges for this CV use counted slot identities. */ + boolean hasCountedCodeSlots; + // Conservative compatibility-index metadata survives ownership retirement + // when embedding code still holds a CV descriptor. It retains no captures. + boolean retiredOwnedEdges; /** Number of queued phaser executions currently invoking this CV. */ int activeEndBlockExecutions; @@ -2960,59 +3494,247 @@ void beginEndBlockExecution() { void endEndBlockExecution() { if (activeEndBlockExecutions > 0) activeEndBlockExecutions--; - if (activeEndBlockExecutions == 0 && rescuedActiveEndBlockOwner) { - rescuedActiveEndBlockOwner = false; - MortalList.deferDecrement(this); + if (activeEndBlockExecutions == 0) { + if (rescuedActiveEndBlockOwner) { + rescuedActiveEndBlockOwner = false; + MortalList.deferDecrement(this); + } + finishActiveCodeOwnerRelease(); } } - /** - * Cached constants referenced via backslash (e.g., \"yay") inside this subroutine. - * When the CODE slot of a glob is replaced, weak references to these constants - * are cleared to emulate Perl 5's "optree reaping" behavior. - */ + /** Cached scalar constants referenced via backslash inside this CV's optree. */ public RuntimeBase[] padConstants; + /** One identity for each strong constant edge held by this optree. */ + private final List padConstantOwnerSlots = new ArrayList<>(); + /** Direct constants for this CV and embedded anonymous optrees. */ + private final Map padConstantArraysByClassName = new LinkedHashMap<>(); + private final Map> padConstantDescendantsByClassName = new LinkedHashMap<>(); + private final Set padConstantRegistryClassNames = new LinkedHashSet<>(); + private String padConstantClassName; + + /** Attach compiled constants and acquire one owner identity per optree edge. */ + public void installPadConstants(RuntimeBase[] constants) { + installPadConstantTree(null, constants, Map.of(), Map.of()); + } + + public void installPadConstantTree( + String className, + RuntimeBase[] constants, + Map embeddedConstants, + Map> embeddedDescendants) { + RuntimeBase[] previousConstants = allPadConstantValues(); + Set previousRegistryNames = new LinkedHashSet<>(padConstantRegistryClassNames); + List previousSlots = new ArrayList<>(padConstantOwnerSlots); + + padConstants = constants == null ? null : constants.clone(); + padConstantClassName = className; + padConstantArraysByClassName.clear(); + padConstantDescendantsByClassName.clear(); + padConstantRegistryClassNames.clear(); + if (className != null) { + padConstantArraysByClassName.put(className, + padConstants == null ? new RuntimeBase[0] : padConstants); + padConstantDescendantsByClassName.put(className, + embeddedConstants == null || embeddedConstants.isEmpty() + ? Collections.emptySet() + : Collections.unmodifiableSet( + new LinkedHashSet<>(embeddedConstants.keySet()))); + padConstantRegistryClassNames.add(className); + } + if (embeddedConstants != null) { + embeddedConstants.forEach((nestedClassName, nestedConstants) -> { + padConstantArraysByClassName.put(nestedClassName, + nestedConstants == null ? new RuntimeBase[0] : nestedConstants.clone()); + padConstantRegistryClassNames.add(nestedClassName); + }); + } + if (embeddedDescendants != null) { + embeddedDescendants.forEach((nestedClassName, descendants) -> + padConstantDescendantsByClassName.put(nestedClassName, + descendants == null || descendants.isEmpty() + ? Collections.emptySet() + : Collections.unmodifiableSet( + new LinkedHashSet<>(descendants)))); + } + padConstantOwnerSlots.clear(); + if (padConstantArraysByClassName.isEmpty()) { + acquirePadConstantOwners(padConstants); + } else { + padConstantArraysByClassName.values().forEach(this::acquirePadConstantOwners); + RuntimeCodeRuntimeState runtimeState = PerlRuntime.current().runtimeCodeState(); + for (String ownedClassName : padConstantArraysByClassName.keySet()) { + runtimeState.padConstantsByClassName.put(ownedClassName, + padConstantArraysByClassName.get(ownedClassName).clone()); + runtimeState.embeddedPadConstantsByClassName.put(ownedClassName, + copyPadConstantArrays(embeddedPadConstantsForClass(ownedClassName))); + runtimeState.embeddedPadConstantDescendantsByClassName.put(ownedClassName, + copyPadConstantDescendants( + padConstantDescendantTreesForClass(ownedClassName))); + } + } - /** - * Registry mapping generated class names to their pad constants. - * Used to transfer pad constants from compile time to runtime for anonymous subs. - */ - /** - * Clears weak references to this subroutine's pad constants. - * Called when the CODE slot of a glob is replaced, emulating Perl 5's - * behavior where replacing a sub frees its op-tree and clears weak refs - * to compile-time constants. - */ - public void clearPadConstantWeakRefs() { - if (padConstants != null) { - for (RuntimeBase constant : padConstants) { + for (PerlOwnerSlot owner : previousSlots) owner.release(); + clearUnownedPadConstantWeakRefs(previousConstants); + removeUnownedPadConstantRegistryEntries(previousRegistryNames); + } + + private void acquirePadConstantOwners(RuntimeBase[] constants) { + if (constants == null) return; + for (RuntimeBase constant : constants) { + if (constant == null) continue; + PerlOwnerSlot owner = new PerlOwnerSlot(PerlOwnerSlot.Kind.CODE_PAD_CONSTANT); + owner.acquireCodePadConstantIdentity(constant); + padConstantOwnerSlots.add(owner); + } + } + + private RuntimeBase[] allPadConstantValues() { + List values = new ArrayList<>(); + if (padConstantArraysByClassName.isEmpty()) { + if (padConstants != null) Collections.addAll(values, padConstants); + } else { + padConstantArraysByClassName.values().forEach(array -> { + if (array != null) Collections.addAll(values, array); + }); + } + return values.toArray(new RuntimeBase[0]); + } + + public RuntimeBase[] padConstantOwnerReferents() { + return allPadConstantValues(); + } + + static void copyPadConstantTree( + RuntimeCode source, + RuntimeCode target, + java.util.function.Function copyValues) { + RuntimeBase[] direct = source.padConstants == null + ? null : copyValues.apply(source.padConstants); + if (source.padConstantClassName == null) { + target.installPadConstants(direct); + return; + } + Map embedded = new LinkedHashMap<>(); + source.embeddedPadConstantsForClass(source.padConstantClassName) + .forEach((name, values) -> embedded.put(name, copyValues.apply(values))); + target.installPadConstantTree(source.padConstantClassName, direct, embedded, + source.padConstantDescendantTreesForClass(source.padConstantClassName)); + } + + /** Transfer this compiled optree's constant owners to its published CV. */ + public void transferPadConstantTreeTo(RuntimeCode target) { + if (target == null || target == this + || (padConstants == null && padConstantClassName == null + && padConstantArraysByClassName.isEmpty())) { + return; + } + copyPadConstantTree(this, target, values -> values.clone()); + clearPadConstantWeakRefs(); + } + + private void releasePadConstantOwners() { + RuntimeBase[] constants = allPadConstantValues(); + Set registryNames = new LinkedHashSet<>(padConstantRegistryClassNames); + List owners = new ArrayList<>(padConstantOwnerSlots); + padConstants = null; + padConstantClassName = null; + padConstantArraysByClassName.clear(); + padConstantDescendantsByClassName.clear(); + padConstantRegistryClassNames.clear(); + padConstantOwnerSlots.clear(); + for (PerlOwnerSlot owner : owners) owner.release(); + Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); + for (RuntimeBase constant : constants) { + if (constant != null && seen.add(constant) + && constant.ownerSlotCount(PerlOwnerSlot.Kind.CODE_PAD_CONSTANT) == 0 + && constant.refCount != Integer.MIN_VALUE + && constant.refCount <= 0) { + // A caller may retain a backslash-addressed literal through an + // uncounted scalar reference after this CV dies. Carry the + // final-release obligation onto that exact slot so its release + // clears weak observers without a later reachability sweep. + constant.deferFinalReleaseToUncountedOwnerSlots(); + } + } + clearUnownedPadConstantWeakRefs(constants); + removeUnownedPadConstantRegistryEntries(registryNames); + } + + private static void clearUnownedPadConstantWeakRefs(RuntimeBase[] constants) { + if (constants == null) return; + Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); + for (RuntimeBase constant : constants) { + if (constant != null && seen.add(constant) + && constant.ownerSlotCount(PerlOwnerSlot.Kind.CODE_PAD_CONSTANT) == 0 + && !constant.hasPerlStrongOwner()) { WeakRefRegistry.clearWeakRefsTo(constant); } } } - /** - * Return whether a readonly scalar is owned by the optree/pad of a - * currently installed named subroutine. - * - *

Pad constants are JVM references that selective refcounting cannot - * observe. This targeted lookup lets mortal processing preserve a weak - * reference until the owning glob is replaced, when - * {@link #clearPadConstantWeakRefs()} performs Perl-compatible optree - * reaping.

- */ - public static boolean isInstalledPadConstant(RuntimeBase target) { - if (target == null) return false; - for (RuntimeScalar codeScalar : GlobalVariable.globalCodeRefValuesView()) { - if (codeScalar == null || !(codeScalar.value instanceof RuntimeCode code) - || code.padConstants == null) { - continue; - } - for (RuntimeBase constant : code.padConstants) { - if (constant == target) return true; + private static void removeUnownedPadConstantRegistryEntries(Set classNames) { + PerlRuntime runtime = PerlRuntime.currentOrNull(); + if (runtime == null || classNames == null || classNames.isEmpty()) return; + RuntimeCodeRuntimeState state = runtime.runtimeCodeState(); + for (String className : classNames) { + RuntimeBase[] constants = state.padConstantsByClassName.get(className); + boolean stillOwned = constants != null && Arrays.stream(constants) + .filter(Objects::nonNull) + .anyMatch(constant -> constant.ownerSlotCount( + PerlOwnerSlot.Kind.CODE_PAD_CONSTANT) != 0); + if (!stillOwned) { + if (constants == null) state.padConstantsByClassName.remove(className); + else state.padConstantsByClassName.remove(className, constants); + state.embeddedPadConstantsByClassName.remove(className); + state.embeddedPadConstantDescendantsByClassName.remove(className); } } - return false; + } + + RuntimeBase[] padConstantsForClass(String className) { + return padConstantArraysByClassName.get(className); + } + + Map embeddedPadConstantsForClass(String className) { + Set descendants = padConstantDescendantsByClassName.get(className); + if (descendants == null || descendants.isEmpty()) return Collections.emptyMap(); + Map result = new LinkedHashMap<>(); + for (String descendant : descendants) { + RuntimeBase[] values = padConstantArraysByClassName.get(descendant); + if (values != null) result.put(descendant, values); + } + return result; + } + + Map> padConstantDescendantTreesForClass(String className) { + Set descendants = padConstantDescendantsByClassName.get(className); + if (descendants == null || descendants.isEmpty()) return Collections.emptyMap(); + Map> result = new LinkedHashMap<>(); + for (String descendant : descendants) { + Set nested = padConstantDescendantsByClassName.get(descendant); + if (nested != null) result.put(descendant, nested); + } + return result; + } + + Map allPadConstantArraysByClassName() { + return Collections.unmodifiableMap(padConstantArraysByClassName); + } + + Map> allPadConstantDescendantsByClassName() { + return Collections.unmodifiableMap(padConstantDescendantsByClassName); + } + + /** Release this CV's optree constant edges and clear observers after the last owner. */ + public void clearPadConstantWeakRefs() { + releasePadConstantOwners(); + } + + /** Return whether an active CODE optree owns this compiled pad constant. */ + public static boolean isInstalledPadConstant(RuntimeBase target) { + return target != null + && target.ownerSlotCount(PerlOwnerSlot.Kind.CODE_PAD_CONSTANT) != 0; } /** @@ -3025,11 +3747,20 @@ public static boolean isInstalledPadConstant(RuntimeBase target) { * captures, those are released recursively. */ public void releaseCaptures() { + if (hasActiveCodeOwner()) { + deferFinalReleaseToActiveCode(); + return; + } if (capturedScalars != null) { + retiredOwnedEdges |= capturedScalars.length != 0; RuntimeScalar[] scalars = capturedScalars; capturedScalars = null; // null out first to prevent re-entry - for (RuntimeScalar s : scalars) { - s.releaseClosureCapture(); + PerlOwnerSlot[] ownerSlots = capturedScalarOwnerSlots; + capturedScalarOwnerSlots = null; + for (int i = 0; i < scalars.length; i++) { + RuntimeScalar s = scalars[i]; + s.releaseClosureCaptureSlot( + ownerSlots != null && i < ownerSlots.length ? ownerSlots[i] : null); if (s.captureCount == 0) { // If the captured scalar itself holds an unowned CODE ref with // captures, release those recursively (handles nested closures). @@ -3060,8 +3791,7 @@ public void releaseCaptures() { continue; } if ((s.type & RuntimeScalarType.REFERENCE_BIT) != 0 - && s.value instanceof RuntimeBase rb - && (rb.blessId != 0 || !WeakRefRegistry.hasWeakRefsTo(rb))) { + && s.value instanceof RuntimeBase) { MortalList.releaseCapturedDecrement(s); } MortalList.noteDeferredCaptureMayBeReady(); @@ -3070,6 +3800,7 @@ public void releaseCaptures() { } } if (capturedAggregates != null) { + retiredOwnedEdges |= capturedAggregates.length != 0; RuntimeBase[] aggregates = capturedAggregates; capturedAggregates = null; PerlOwnerSlot[] ownerSlots = capturedAggregateOwnerSlots; @@ -3166,6 +3897,10 @@ public RuntimeCode cloneForClosure() { clone.isDeclared = this.isDeclared; clone.hasBodyDefinition = this.hasBodyDefinition; clone.constantValue = this.constantValue; + copyPadConstantTree(this, clone, values -> values.clone()); + // The closure prototype is no longer callable after this clone is + // installed. Transfer its optree edges to the callable CV. + clearPadConstantWeakRefs(); clone.lexicalVariableNames = this.lexicalVariableNames == null ? null : new java.util.LinkedHashSet<>(this.lexicalVariableNames); clone.ourVariableRegistry = this.ourVariableRegistry == null @@ -3877,6 +4612,7 @@ public static void copy(RuntimeCode code, RuntimeCode codeFrom) { code.deparseFlags = codeFrom.deparseFlags; code.deparseSourceOffset = codeFrom.deparseSourceOffset; code.deparseSourceEnd = codeFrom.deparseSourceEnd; + copyPadConstantTree(codeFrom, code, values -> values.clone()); } public void adoptDefinitionFrom(RuntimeCode codeFrom) { @@ -3949,6 +4685,7 @@ public void adoptDefinitionFrom(RuntimeCode codeFrom) { this.stateVariable = codeFrom.stateVariable; this.stateArray = codeFrom.stateArray; this.stateHash = codeFrom.stateHash; + this.stateAggregateAliasOwners = codeFrom.stateAggregateAliasOwners; this.constantValue = codeFrom.constantValue; if (sourceNeedsDelegation) { this.subroutine = (runtimeArgs, callContext) -> @@ -3960,6 +4697,7 @@ public void adoptDefinitionFrom(RuntimeCode codeFrom) { } this.__SUB__ = codeFrom.__SUB__; this.capturedScalars = codeFrom.capturedScalars; + this.capturedScalarOwnerSlots = codeFrom.capturedScalarOwnerSlots; this.capturedAggregates = codeFrom.capturedAggregates; this.captureFieldsRecorded = codeFrom.captureFieldsRecorded; this.capturedAggregateOwnerSlots = codeFrom.capturedAggregateOwnerSlots; @@ -3969,7 +4707,8 @@ public void adoptDefinitionFrom(RuntimeCode codeFrom) { this.lexicalDisabledWarningCategories = codeFrom.lexicalDisabledWarningCategories; this.ourVariableRegistry = codeFrom.ourVariableRegistry; this.declaredOurVariables = codeFrom.declaredOurVariables; - this.padConstants = codeFrom.padConstants; + copyPadConstantTree(codeFrom, this, values -> values.clone()); + rebindCaptureOwnerSlots(); } /** @@ -5492,14 +6231,25 @@ public static RuntimeScalar makeCodeObject( code.deparseSourceOffset = deparseSourceOffset; code.deparseSourceEnd = deparseSourceEnd; - // Look up pad constants registered at compile time for this class. - // These track cached string literals referenced via \ inside the sub, - // needed for optree reaping (clearing weak refs when sub is replaced). + // Transfer the compiled optree's backslash-addressable constants to + // this CV so they receive owner identities with the rest of its body. String internalClassName = clazz.getName().replace('.', '/'); - RuntimeBase[] padConsts = PerlRuntime.current().runtimeCodeState() - .padConstantsByClassName.remove(internalClassName); - if (padConsts != null) { - code.padConstants = padConsts; + RuntimeCodeRuntimeState runtimeCodeState = PerlRuntime.current().runtimeCodeState(); + RuntimeCode activeCode = getActiveCodeAt(0); + boolean activeCodeOwnsTree = activeCode != null + && activeCode.padConstantsForClass(internalClassName) != null; + RuntimeBase[] padConsts = activeCodeOwnsTree + ? activeCode.padConstantsForClass(internalClassName) + : runtimeCodeState.padConstantsByClassName.get(internalClassName); + Map embeddedPadConsts = activeCodeOwnsTree + ? activeCode.embeddedPadConstantsForClass(internalClassName) + : runtimeCodeState.embeddedPadConstantsByClassName.get(internalClassName); + Map> embeddedPadDescendants = activeCodeOwnsTree + ? activeCode.padConstantDescendantTreesForClass(internalClassName) + : runtimeCodeState.embeddedPadConstantDescendantsByClassName.get(internalClassName); + if (padConsts != null || embeddedPadConsts != null && !embeddedPadConsts.isEmpty()) { + code.installPadConstantTree(internalClassName, padConsts, + embeddedPadConsts, embeddedPadDescendants); GlobalVariable.invalidateGlobalCodeRefGraphRoots(); } Set disabledWarnings = PerlRuntime.current().runtimeCodeState() @@ -5512,6 +6262,7 @@ public static RuntimeScalar makeCodeObject( // can decrement blessed ref refCounts when the closure is discarded. Field[] allFields = clazz.getDeclaredFields(); List captured = new ArrayList<>(); + List capturedScalarOwnerSlots = new ArrayList<>(); List capturedAggregates = new ArrayList<>(); List capturedAggregateOwnerSlots = new ArrayList<>(); Set seenScalars = Collections.newSetFromMap(new IdentityHashMap<>()); @@ -5526,7 +6277,7 @@ public static RuntimeScalar makeCodeObject( } code.closedOverVariables.put(f.getName(), capturedVar); captured.add(capturedVar); - capturedVar.retainClosureCapture(); + capturedScalarOwnerSlots.add(capturedVar.retainClosureCaptureSlot(code)); } } else if (f.getType() == RuntimeArray.class || f.getType() == RuntimeHash.class) { RuntimeBase capturedAggregate = (RuntimeBase) f.get(codeObject); @@ -5537,7 +6288,7 @@ public static RuntimeScalar makeCodeObject( } code.closedOverVariables.put(f.getName(), capturedAggregate); capturedAggregates.add(capturedAggregate); - PerlOwnerSlot ownerSlot = capturedAggregate.retainClosureCaptureOwner(); + PerlOwnerSlot ownerSlot = capturedAggregate.retainClosureCaptureOwner(code); capturedAggregateOwnerSlots.add(ownerSlot); } } @@ -5545,6 +6296,8 @@ public static RuntimeScalar makeCodeObject( code.captureFieldsRecorded = true; if (!captured.isEmpty()) { code.capturedScalars = captured.toArray(new RuntimeScalar[0]); + code.capturedScalarOwnerSlots = + capturedScalarOwnerSlots.toArray(new PerlOwnerSlot[0]); } if (!capturedAggregates.isEmpty()) { code.capturedAggregates = capturedAggregates.toArray(new RuntimeBase[0]); @@ -5662,7 +6415,7 @@ public static RuntimeList call(RuntimeScalar runtimeScalar, CallArguments args, int callContext) { try { - return call(runtimeScalar, method, currentSub, args.argumentValues(), callContext); + return call(runtimeScalar, method, currentSub, args.toRuntimeArray(), callContext); } finally { args.finish(); } @@ -5677,7 +6430,7 @@ public static RuntimeList callCached(int callsiteId, int callContext) { try { return callCached(callsiteId, runtimeScalar, method, currentSub, - args.argumentValues(), callContext); + args.toRuntimeArray(), callContext); } finally { args.finish(); } @@ -6227,8 +6980,10 @@ private static RuntimeList bindReturnedMethodCoderef( bound.subName = target.subName; bound.refCount = 0; bound.capturedScalars = new RuntimeScalar[] { new RuntimeScalar(codeRef), invocant }; - for (RuntimeScalar captured : bound.capturedScalars) { - captured.retainClosureCapture(); + bound.capturedScalarOwnerSlots = new PerlOwnerSlot[bound.capturedScalars.length]; + for (int i = 0; i < bound.capturedScalars.length; i++) { + bound.capturedScalarOwnerSlots[i] = + bound.capturedScalars[i].retainClosureCaptureSlot(bound); } registerJvmClosure(bound); return new RuntimeScalar(bound).getList(); @@ -6660,7 +7415,7 @@ public static RuntimeList callerWithSub(RuntimeList args, int ctx, RuntimeScalar RuntimeArray frameArgs = DebugState.getArgsForFrame(frame); if (frameArgs != null) { frameArgs.clearStaleLocalArrayAliases(); - dbArgs.setFromListAliased(frameArgs.getList()); + dbArgs.setFromListAliased(new RuntimeList(frameArgs.elements)); } else { dbArgs.setFromListAliased(new RuntimeList()); } @@ -6686,7 +7441,7 @@ public static RuntimeList callerWithSub(RuntimeList args, int ctx, RuntimeScalar } if (frameArgs != null) { frameArgs.clearStaleLocalArrayAliases(); - dbArgs.setFromListAliased(frameArgs.getList()); + dbArgs.setFromListAliased(new RuntimeList(frameArgs.elements)); } else { dbArgs.setFromListAliased(new RuntimeList()); } @@ -7842,6 +8597,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int RuntimeScalar nextTailCode = null; RuntimeArray nextTailArgs = null; boolean returnedTailCall = false; + boolean flushAfterScopeExit = false; boolean cleanupArgsAfterCall = curArgsFromTailCall; RuntimeScalar codeRefForCall = curScalar; RuntimeArray argsForCall = curArgs; @@ -7905,7 +8661,12 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int + flow.marker.fileName + " line " + line + ".\n"); } MyVarCleanupStack.unwindTo(cleanupMark); - MortalList.flush(); + // A non-local return still transfers its value through + // the enclosing call. Its mortals cannot be drained + // until that receiving boundary has acquired them. + if (flowType != ControlFlowType.RETURN) { + MortalList.flush(); + } } // Mortal-ize blessed refs with refCount==0 in void-context calls. // These are objects that were created but never stored in a named @@ -7933,7 +8694,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int // Reference-valued results deliberately remain deferred until the // caller has materialized them into their destination. if (containsNoReference(returned)) { - MortalList.flushAboveMark(); + flushAfterScopeExit = true; } MyVarCleanupStack.releaseOrTransferSocketOwnersOnReturn( cleanupMark, returned); @@ -7951,7 +8712,17 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int // Perl treats `return` inside eval { ... } as the eval value, // including when it originates from a nested map/grep block. RuntimeList result = e.returnValue != null ? e.returnValue.getList() : new RuntimeList(); - return coerceScalarCallResult(result, effectiveContext, callContext, !isLvalueCode(code)); + RuntimeList returned = coerceScalarCallResult( + result, effectiveContext, callContext, !isLvalueCode(code)); + if (effectiveContext == RuntimeContextType.VOID) { + MortalList.mortalizeForVoidDiscard(returned); + flushAfterScopeExit = true; + } else if (containsNoReference(returned)) { + flushAfterScopeExit = true; + } + MyVarCleanupStack.releaseOrTransferSocketOwnersOnReturn( + cleanupMark, returned); + return returned; } catch (RuntimeException e) { e = WarnDie.maybeInvokeUnhandledDieHandler(e); // On die: run scopeExitCleanup for my-variables whose normal @@ -7965,22 +8736,24 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int throw e; } finally { if (returnedTailCall) { - // A goto &sub retires this frame before the replacement - // executes. Drain only this frame's scope exits, not the - // caller's deferred coderef metadata. - MortalList.flushAboveMark(); + flushAfterScopeExit = true; } if (cleanupArgsAfterCall) { cleanupTailCallArgs(argsForCall); cleanupTailCallCodeRef(codeRefForCall); } - // Pop the function-scoped mortal mark. Entries added by this - // function's scope-exit cleanup "fall" to the caller's scope - // and will be processed by the caller's flushAboveMark(). - MortalList.popMark(); - // After unwindTo, entries are already removed; popMark is a no-op. - // On normal return, popMark discards registrations without cleanup. - MyVarCleanupStack.popMark(cleanupMark); + if (flushAfterScopeExit) { + // Release the frame's lexical owners before processing the + // decrements they queued, so a stale registration cannot + // keep an explicitly returned capture alive. + MyVarCleanupStack.popMark(cleanupMark); + MortalList.flushAboveMark(); + MortalList.popMark(); + } else { + // Entries added by ordinary scope exit fall to the caller. + MortalList.popMark(); + MyVarCleanupStack.popMark(cleanupMark); + } WarningBitsRegistry.setCallSiteHints(compilationState, callerCallSiteHints); WarningBitsRegistry.setCallSiteBits(callerCallSiteWarningBits); HintHashRegistry.popCallerHintHash(compilationState); @@ -8604,6 +9377,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa int cleanupMark = MyVarCleanupStack.pushMark(); MortalList.pushMark(); boolean returnedTailCall = false; + boolean flushAfterScopeExit = false; try { // Cast the value to RuntimeCode and call apply() RuntimeList result = code.apply(subroutineName, a, callContext); @@ -8613,9 +9387,9 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa // See the 3-arg apply() overload for detailed rationale. if (effectiveContext == RuntimeContextType.VOID) { MortalList.mortalizeForVoidDiscard(result); - MortalList.flushAboveMark(); + flushAfterScopeExit = true; } else if (containsNoReference(result)) { - MortalList.flushAboveMark(); + flushAfterScopeExit = true; } MyVarCleanupStack.releaseOrTransferSocketOwnersOnReturn( cleanupMark, result); @@ -8640,14 +9414,25 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa throw e; } finally { if (returnedTailCall) { - MortalList.flushAboveMark(); + flushAfterScopeExit = true; } if ("tailcall".equals(subroutineName)) { cleanupTailCallArgs(a); cleanupTailCallCodeRef(runtimeScalar); } - MortalList.popMark(); - MyVarCleanupStack.popMark(cleanupMark); + if (flushAfterScopeExit) { + // Drain return-path decrements only after the callee's + // lexical registrations are gone. Otherwise a captured + // blessed referent can be counted as a live lexical + // owner during this flush and survive until the + // corrective scope-exit sweep. + MyVarCleanupStack.popMark(cleanupMark); + MortalList.flushAboveMark(); + MortalList.popMark(); + } else { + MortalList.popMark(); + MyVarCleanupStack.popMark(cleanupMark); + } WarningBitsRegistry.setCallSiteHints(compilationState, callerCallSiteHints); WarningBitsRegistry.setCallSiteBits(callerCallSiteWarningBits); HintHashRegistry.popCallerHintHash(compilationState); @@ -8766,7 +9551,8 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, CallArguments args, int callContext) { try { - return apply(runtimeScalar, subroutineName, args.argumentValues(), callContext); + return apply(runtimeScalar, subroutineName, + (RuntimeBase) args.toRuntimeArray(), callContext); } finally { args.finish(); } @@ -9508,25 +10294,43 @@ public static RuntimeScalar materializeBlockResult(RuntimeScalar result) { * aliasing its former storage when it crosses the do-block boundary. */ public static RuntimeScalar copyDoBlockResult(RuntimeScalar result) { + if ((result.type & RuntimeScalarType.REFERENCE_BIT) != 0) { + return RuntimeScalar.copyToMortalReturnSlot(result); + } return new RuntimeScalar(result); } /** Copy a do-block result used in list context without collapsing the list. */ public static RuntimeBase copyDoBlockListResult(RuntimeBase result) { if (result instanceof RuntimeScalar scalar) { - return new RuntimeScalar(scalar); + return copyDoBlockScalarResult(scalar); + } + if (result instanceof RuntimeCode code) { + return RuntimeScalar.copyToMortalReturnSlot(new RuntimeScalar(code)); } if (result instanceof RuntimeList list) { RuntimeList copy = new RuntimeList(); for (RuntimeBase element : list.elements) { - copy.elements.add(element instanceof RuntimeScalar scalar - ? new RuntimeScalar(scalar) : element); + if (element instanceof RuntimeScalar scalar) { + copy.elements.add(copyDoBlockScalarResult(scalar)); + } else if (element instanceof RuntimeCode code) { + copy.elements.add(RuntimeScalar.copyToMortalReturnSlot( + new RuntimeScalar(code))); + } else { + copy.elements.add(element); + } } return copy; } return result; } + private static RuntimeScalar copyDoBlockScalarResult(RuntimeScalar scalar) { + return (scalar.type & RuntimeScalarType.REFERENCE_BIT) != 0 + ? RuntimeScalar.copyToMortalReturnSlot(scalar) + : new RuntimeScalar(scalar); + } + public boolean defined() { // Built-in operators are always considered "defined" if (this.isBuiltin) { @@ -9863,6 +10667,7 @@ private RuntimeList apply(RuntimeArray a, int callContext, PerlRuntime cachedRun } popActiveCode(this, executionState); popArgs(executionState); // also pops hasArgsStack — see popArgs() implementation + finishActiveCodeOwnerRelease(); if (DebugState.isDebugMode()) { DebugHooks.exitSubroutine(); DebugState.popArgs(); @@ -10039,6 +10844,7 @@ public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) } popActiveCode(this); popArgs(); // also pops hasArgsStack — see popArgs() implementation + finishActiveCodeOwnerRelease(); if (DebugState.isDebugMode()) { DebugHooks.exitSubroutine(); DebugState.popArgs(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCodeRuntimeState.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCodeRuntimeState.java index d27a6bba48..9797b249ca 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCodeRuntimeState.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCodeRuntimeState.java @@ -27,6 +27,10 @@ public final class RuntimeCodeRuntimeState { final HashMap evalContexts = new HashMap<>(); final ConcurrentHashMap padConstantsByClassName = new ConcurrentHashMap<>(); + final ConcurrentHashMap> embeddedPadConstantsByClassName = + new ConcurrentHashMap<>(); + final ConcurrentHashMap>> embeddedPadConstantDescendantsByClassName = + new ConcurrentHashMap<>(); final ConcurrentHashMap> disabledWarningsByClassName = new ConcurrentHashMap<>(); @@ -117,6 +121,8 @@ void clearCaches() { interpretedSubs.clear(); evalContexts.clear(); padConstantsByClassName.clear(); + embeddedPadConstantsByClassName.clear(); + embeddedPadConstantDescendantsByClassName.clear(); disabledWarningsByClassName.clear(); nextMethodCallsiteId = 0; clearInlineMethodCache(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index 26280eb418..bd9c48a286 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -635,6 +635,7 @@ public RuntimeScalar set(RuntimeScalar value) { } // Get or create the code ref container RuntimeScalar codeContainer = GlobalVariable.defineGlobalCodeRef(codeSlotName); + RuntimeCode replacedStashCode = null; // A non-local typeglob assignment is a redefinition for // prototype purposes too. Parser-time checks do not see @@ -739,15 +740,15 @@ public RuntimeScalar set(RuntimeScalar value) { return value; } - // Before overwriting, clear weak refs to the old sub's pad constants. - // This emulates Perl 5's behavior where replacing a sub frees its op-tree, - // causing compile-time constants to be freed and weak refs to be cleared. + // Replacing this glob drops only its CODE-slot owner. A saved + // coderef may still keep the old CV and its optree constants alive; + // final CV cleanup releases those constants after the last owner leaves. if (codeContainer.value instanceof RuntimeCode oldCode) { - oldCode.clearPadConstantWeakRefs(); // Decrement stashRefCount on the old CODE ref being replaced if (oldCode.stashRefCount > 0) { oldCode.stashRefCount--; } + replacedStashCode = oldCode; } // A BEGIN-installed `sub () { $lexical }` becomes a constant @@ -780,6 +781,9 @@ public RuntimeScalar set(RuntimeScalar value) { newCode.hadStashRef = true; newCode.stashRefCount++; } + if (replacedStashCode != null) { + replacedStashCode.noteStashOwnerReleased(); + } // Invalidate the method resolution cache InheritanceResolver.invalidateCache(); @@ -1952,23 +1956,17 @@ public RuntimeGlob undefine() { } // Undefine CODE RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRef(this.globName); - RuntimeCode displacedCode = codeRef.value instanceof RuntimeCode code ? code : null; - boolean displacedCodeHadWalkableEdges = displacedCode != null - && ReachabilityWalker.hasWalkableCodeEdges(displacedCode, true); + RuntimeCode removedStashCode = codeRef.value instanceof RuntimeCode code + ? code : null; codeRef.set(new RuntimeScalar()); + if (removedStashCode != null && removedStashCode.stashRefCount > 0) { + removedStashCode.stashRefCount--; + removedStashCode.noteStashOwnerReleased(); + } // The CODE scalar can be pinned separately from the stash-map entry. // Undefining a whole glob must invalidate root indexes even when that // scalar was not marked as a package-global root itself. GlobalVariable.noteGlobalCodeRefSlotMutation(codeRef); - // A CODE-owned state variable can be the only strong Perl owner of an - // unblessed object with weak observers. Removing the glob drops that - // owner immediately in Perl, so clear weak observers before returning - // instead of waiting for a later statement-boundary sweep. - if (displacedCodeHadWalkableEdges - && WeakRefRegistry.weakRefsExist() - && !ModuleInitGuard.inModuleInit()) { - ReachabilityWalker.sweepWeakRefs(true); - } // Invalidate the method resolution cache InheritanceResolver.invalidateCache(); @@ -2156,6 +2154,8 @@ public void dynamicSaveState() { } RuntimeScalar newCode = new RuntimeScalar(); GlobalVariable.markPackageGlobalRoot(newCode); + // The saved GP owns its CV before the last visible name is replaced. + GlobalVariable.enterLocalizedCodeRef(this.globName, savedCode); // A localized whole-glob alias still addresses one temporary CODE // slot. In particular, `*A = *Base::method; local *A = sub { ... }` // must make Base::method visible to MRO until the scope unwinds. @@ -2168,6 +2168,7 @@ public void dynamicSaveState() { if (savedCodeObj.stashRefCount > 0) { savedCodeObj.stashRefCount--; } + savedCodeObj.noteStashOwnerReleased(); } // Also redirect pinnedCodeRefs to the new empty code for the local scope. // Without this, getGlobalCodeRef() returns the saved (pinned) object, and @@ -2176,7 +2177,6 @@ public void dynamicSaveState() { for (String alias : codeAliasGroup) { GlobalVariable.replacePinnedCodeRef(alias, newCode); } - GlobalVariable.enterLocalizedCodeRef(this.globName, savedCode); GlobalVariable.getGlobalFormatRef(this.globName).dynamicSaveState(); // Create a NEW RuntimeGlob for the local scope and install it in globalIORefs. @@ -2302,11 +2302,14 @@ && hasDestroyMethod(displacedBase)) { // have permanently overcounted refCount, preventing releaseCaptures // from firing at the right time. RuntimeScalar localCode = GlobalVariable.globalCodeRefs.get(snap.globName); - RuntimeCode removedLocalCodeForReachabilityCheck = null; - if (localCode != null && (localCode.type & REFERENCE_BIT) != 0 && localCode.value instanceof RuntimeBase localBase) { + RuntimeCode removedLocalCodeForStashRelease = null; + if (localCode != null && (localCode.type & REFERENCE_BIT) != 0 + && localCode.value instanceof RuntimeBase localBase + && !(localBase instanceof RuntimeCode code && code.hasCountedCodeSlots)) { boolean removedLastStashRef = false; // Decrement stashRefCount on the local scope's CODE ref being removed if (localBase instanceof RuntimeCode localCodeObj) { + removedLocalCodeForStashRelease = localCodeObj; if (localCodeObj.stashRefCount > 0) { localCodeObj.stashRefCount--; removedLastStashRef = localCodeObj.stashRefCount == 0; @@ -2321,7 +2324,7 @@ && hasDestroyMethod(displacedBase)) { } else if (localBase instanceof RuntimeCode localCodeObj && removedLastStashRef && (localCodeObj.capturedScalars != null || localCodeObj.capturedAggregates != null)) { - removedLocalCodeForReachabilityCheck = localCodeObj; + removedLocalCodeForStashRelease = localCodeObj; } } for (var entry : snap.aliasedCodes.entrySet()) { @@ -2333,19 +2336,14 @@ && hasDestroyMethod(displacedBase)) { } } GlobalVariable.invalidatePackageRootSnapshot(); - if (removedLocalCodeForReachabilityCheck != null - && removedLocalCodeForReachabilityCheck.activeOwnerCount() == 0 - && !ReachabilityWalker.isReachableFromLiveScalarRegistry(removedLocalCodeForReachabilityCheck) - && !ReachabilityWalker.isReachableFromLiveCodeCaptures(removedLocalCodeForReachabilityCheck) - && !ReachabilityWalker.isReachableFromRoots(removedLocalCodeForReachabilityCheck)) { - removedLocalCodeForReachabilityCheck.refCount = Integer.MIN_VALUE; - DestroyDispatch.callDestroy(removedLocalCodeForReachabilityCheck); - } // Increment stashRefCount on the restored CODE ref being put back in the stash if (snap.code != null && snap.code.value instanceof RuntimeCode restoredCode) { restoredCode.hadStashRef = true; restoredCode.stashRefCount++; } + if (removedLocalCodeForStashRelease != null) { + removedLocalCodeForStashRelease.noteStashOwnerReleased(); + } // Also restore the pinned code ref so getGlobalCodeRef() returns the // original code object again. for (var entry : snap.aliasedCodes.entrySet()) { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGraphCloner.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGraphCloner.java index d7f02304e2..f3369ce47b 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGraphCloner.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGraphCloner.java @@ -164,6 +164,14 @@ void finishSnapshot() { finishCloneBoundary(); } + void restoreCodeSlotBindings(java.util.Collection slots) { + try (PerlRuntime.Binding ignored = targetRuntime.bind()) { + for (RuntimeScalar slot : slots) { + if (slot != null) slot.retainCodeSlotBinding(); + } + } + } + /** Package/runtime snapshot entry point that retains the shared graph map. */ RuntimeBase cloneValue(RuntimeBase value) { if (value == null) return null; @@ -173,6 +181,17 @@ RuntimeBase cloneValue(RuntimeBase value) { // shared synchronization identity. if (value.threadShared && !needsSharedRuntimeView(value)) return value; Object existing = clones.get(value); + if (existing instanceof RuntimeScalar clonedScalar + && clonedScalar.refCount == Integer.MIN_VALUE + && value instanceof RuntimeScalar sourceScalar + && sourceScalar.refCount != Integer.MIN_VALUE) { + // A lazy CODE definition can expose a pad cell after the initial + // thread snapshot. If this snapshot already cloned that source + // cell and the child has since retired the clone, reuse would + // attach the new closure metadata to a destroyed cell. + clones.remove(value); + existing = null; + } if (existing != null) return (RuntimeBase) existing; if (value instanceof RuntimeScalarReadOnly scalar @@ -199,6 +218,8 @@ protected RuntimeBase cloneCode(RuntimeCode source) { if (existing != null) return (RuntimeCode) existing; RuntimeCode target; + RuntimeBase[] captureValues = null; + InterpretedCode captureOwnerBody = null; if (source instanceof InterpretedCode interpreted) { // cloneInterpretedCode copies metadata itself because it is also // used for an interpreted body nested inside a lazy RuntimeCode @@ -213,12 +234,15 @@ protected RuntimeBase cloneCode(RuntimeCode source) { // the parent's capturedVars array even though the placeholder's // metadata had been cloned correctly. target = new RuntimeCode((PerlSubroutine) null, source.prototype); + copyBase(source, target); clones.put(source, target); InterpretedCode clonedBody = cloneInterpretedCode(interpreted); target.subroutine = clonedBody; target.codeObject = clonedBody; + captureOwnerBody = clonedBody; } else { target = new RuntimeCode((PerlSubroutine) null, source.prototype); + copyBase(source, target); clones.put(source, target); if (source.subroutine instanceof CloneablePerlSubroutine cloneable) { RuntimeBase[] captures = cloneable.capturedValues(); @@ -229,6 +253,7 @@ protected RuntimeBase cloneCode(RuntimeCode source) { CloneablePerlSubroutine clonedSubroutine = cloneable.cloneWithCaptures(clonedCaptures); target.subroutine = clonedSubroutine; target.codeObject = clonedSubroutine; + captureValues = clonedCaptures; } else { // Java/native PerlSubroutine implementations carry no Perl pad. target.subroutine = source.subroutine; @@ -236,7 +261,20 @@ protected RuntimeBase cloneCode(RuntimeCode source) { target.methodHandle = source.methodHandle; } } - copyCodeMetadata(source, target); + copyCodeMetadata(source, target, captureValues, captureOwnerBody == null); + if (captureOwnerBody != null) { + // The wrapper and its interpreted body are one Perl CV. Share the + // body's capture-owner identities instead of retaining each cell + // once for the body and again for its wrapper. + target.capturedScalars = captureOwnerBody.capturedScalars; + target.capturedScalarOwnerSlots = captureOwnerBody.capturedScalarOwnerSlots; + target.capturedAggregates = captureOwnerBody.capturedAggregates; + target.capturedAggregateOwnerSlots = captureOwnerBody.capturedAggregateOwnerSlots; + if (target.capturedScalars != null || target.capturedAggregates != null) { + target.refCount = 0; + } + target.rebindCaptureOwnerSlots(); + } return target; } @@ -280,6 +318,7 @@ private InterpretedCode cloneInterpretedCode(InterpretedCode source) { source.evalSitePragmaFlags, source.warningBitsString); } + copyBase(source, target); clones.put(source, target); target.ourVariableRegistry = source.ourVariableRegistry; target.declaredOurVariables = source.declaredOurVariables; @@ -291,7 +330,7 @@ private InterpretedCode cloneInterpretedCode(InterpretedCode source) { target.signatureSubName = source.signatureSubName; target.gotoLabelPcs = source.gotoLabelPcs; target.gotoLabelsInsideLoop = source.gotoLabelsInsideLoop; - copyCodeMetadata(source, target); + copyCodeMetadata(source, target, captures); return target; } @@ -303,6 +342,16 @@ private RuntimeBase[] cloneRuntimeBases(RuntimeBase[] values) { } private void copyCodeMetadata(RuntimeCode source, RuntimeCode target) { + copyCodeMetadata(source, target, null, true); + } + + private void copyCodeMetadata( + RuntimeCode source, RuntimeCode target, RuntimeBase[] captureValues) { + copyCodeMetadata(source, target, captureValues, true); + } + + private void copyCodeMetadata(RuntimeCode source, RuntimeCode target, + RuntimeBase[] captureValues, boolean copyCaptureOwnership) { target.prototype = source.prototype; target.attributes = source.attributes == null ? null : new ArrayList<>(source.attributes); target.packageName = source.packageName; @@ -364,7 +413,12 @@ private void copyCodeMetadata(RuntimeCode source, RuntimeCode target) { clones.put(source, target); } } - target.adoptDefinitionFrom(completed); + try (PerlRuntime.Binding ignored = targetRuntime.bind()) { + target.adoptDefinitionFrom(completed); + // The materialized child copy was an intermediate CV; + // the installed placeholder now owns its pad constants. + completed.clearPadConstantWeakRefs(); + } target.compilerSupplier = null; } finally { org.perlonjava.app.scriptengine.PerlLanguageProvider.COMPILE_LOCK.unlock(); @@ -382,9 +436,11 @@ private void copyCodeMetadata(RuntimeCode source, RuntimeCode target) { target.lexicalSubDisplayName = source.lexicalSubDisplayName; target.isConstantCv = source.isConstantCv; target.isLexicalConstantCv = source.isLexicalConstantCv; + RuntimeCode.copyPadConstantTree(source, target, this::cloneRuntimeBases); target.stashInstallPackage = source.stashInstallPackage; target.stashInstallSub = source.stashInstallSub; target.hadStashRef = source.hadStashRef; + target.hasCountedCodeSlots = source.hasCountedCodeSlots; target.explicitlyMaterializedGlob = source.explicitlyMaterializedGlob; target.captureFieldsRecorded = source.captureFieldsRecorded; target.installedViaAnonGlobAssign = source.installedViaAnonGlobAssign; @@ -406,6 +462,7 @@ private void copyCodeMetadata(RuntimeCode source, RuntimeCode target) { target.stateVariable = cloneScalarMap(source.stateVariable); target.stateArray = cloneArrayMap(source.stateArray); target.stateHash = cloneHashMap(source.stateHash); + target.stateAggregateAliasOwners = cloneScalarMap(source.stateAggregateAliasOwners); if (source.__SUB__ != null) { target.restoreClonedSelfReference( (RuntimeScalar) cloneValue(source.__SUB__)); @@ -426,39 +483,81 @@ private void copyCodeMetadata(RuntimeCode source, RuntimeCode target) { } } - // capturedScalars/capturedAggregates are the authoritative ownership - // lists built by makeCodeObject()/CREATE_CLOSURE. closedOverVariables - // is diagnostic/name metadata and can legitimately omit executable - // captures, so rebuilding ownership from that map allowed a method - // return to DESTROY a still-live captured object in an ithread. - if (source.capturedScalars != null) { - target.capturedScalars = new RuntimeScalar[source.capturedScalars.length]; - for (int i = 0; i < source.capturedScalars.length; i++) { - target.capturedScalars[i] = - (RuntimeScalar) cloneValue(source.capturedScalars[i]); - } - try (PerlRuntime.Binding ignored = targetRuntime.bind()) { - for (RuntimeScalar captured : target.capturedScalars) { - captured.retainThreadCloneClosureCapture(); + // The executable closure fields / bytecode capturedVars are the + // authoritative captured cells. Re-cloning RuntimeCode's parallel + // metadata array can create a second scalar cell for the same source + // capture; its legacy capture bridge then retains the referent after + // the executable cell is assigned undef in an ithread. + if (copyCaptureOwnership) { + List clonedCaptureScalars = new ArrayList<>(); + List clonedCaptureAggregates = new ArrayList<>(); + Set seenCaptureValues = java.util.Collections.newSetFromMap( + new IdentityHashMap<>()); + if (captureValues != null) { + for (RuntimeBase capture : captureValues) { + // Preserve the bytecode capture indexes, but do not create + // ownership metadata for a dead cell left in a lazy CV's + // conservative capture array. Such cells are placeholders, + // not live Perl captures. + if (capture == null || capture.refCount == Integer.MIN_VALUE + || !seenCaptureValues.add(capture)) continue; + if (capture instanceof RuntimeScalar scalar) { + clonedCaptureScalars.add(scalar); + } else if (capture instanceof RuntimeArray || capture instanceof RuntimeHash) { + clonedCaptureAggregates.add(capture); + } + } + } else { + RuntimeScalar[] sourceCapturedScalars = source.capturedScalars; + if (sourceCapturedScalars != null) { + for (RuntimeScalar scalar : sourceCapturedScalars) { + // Conservative capture metadata can outlive the pad + // cell it once described, especially while lazy named + // CODE definitions are copied into an ithread. A dead + // cell is not a Perl owner and must not be resurrected + // as a child closure capture. + if (scalar == null || scalar.refCount == Integer.MIN_VALUE) continue; + RuntimeScalar cloned = (RuntimeScalar) cloneValue(scalar); + if (cloned != null && cloned.refCount != Integer.MIN_VALUE + && seenCaptureValues.add(cloned)) { + clonedCaptureScalars.add(cloned); + } + } + } + if (source.capturedAggregates != null) { + for (RuntimeBase aggregate : source.capturedAggregates) { + RuntimeBase cloned = cloneValue(aggregate); + if (cloned != null && seenCaptureValues.add(cloned)) { + clonedCaptureAggregates.add(cloned); + } + } } } - } - if (source.capturedAggregates != null) { - target.capturedAggregates = new RuntimeBase[source.capturedAggregates.length]; - for (int i = 0; i < source.capturedAggregates.length; i++) { - target.capturedAggregates[i] = cloneValue(source.capturedAggregates[i]); + if (!clonedCaptureScalars.isEmpty()) { + target.capturedScalars = clonedCaptureScalars.toArray(new RuntimeScalar[0]); + try (PerlRuntime.Binding ignored = targetRuntime.bind()) { + target.capturedScalarOwnerSlots = + new PerlOwnerSlot[target.capturedScalars.length]; + for (int i = 0; i < target.capturedScalars.length; i++) { + target.capturedScalarOwnerSlots[i] = target.capturedScalars[i] + .retainThreadCloneClosureCapture(target); + } + } } - try (PerlRuntime.Binding ignored = targetRuntime.bind()) { - target.capturedAggregateOwnerSlots = - new PerlOwnerSlot[target.capturedAggregates.length]; - for (int i = 0; i < target.capturedAggregates.length; i++) { - target.capturedAggregateOwnerSlots[i] = - target.capturedAggregates[i].retainClosureCaptureOwner(); + if (!clonedCaptureAggregates.isEmpty()) { + target.capturedAggregates = clonedCaptureAggregates.toArray(new RuntimeBase[0]); + try (PerlRuntime.Binding ignored = targetRuntime.bind()) { + target.capturedAggregateOwnerSlots = + new PerlOwnerSlot[target.capturedAggregates.length]; + for (int i = 0; i < target.capturedAggregates.length; i++) { + target.capturedAggregateOwnerSlots[i] = + target.capturedAggregates[i].retainClosureCaptureOwner(target); + } } } - } - if (target.capturedScalars != null || target.capturedAggregates != null) { - target.refCount = 0; + if (target.capturedScalars != null || target.capturedAggregates != null) { + target.refCount = 0; + } } } @@ -598,6 +697,7 @@ private void restoreScalarReferenceOwnership(RuntimeScalar source, RuntimeScalar referent.hadCountedReference = true; target.refCountOwned = true; try (PerlRuntime.Binding ignored = targetRuntime.bind()) { + target.recordCountedOwner(referent); ScalarRefRegistry.registerRef(target); } } @@ -860,6 +960,10 @@ private void copyBase(RuntimeBase source, RuntimeBase target) { target.blessId = cloneBlessId(source.blessId); } target.localBindingExists = source.localBindingExists; + // Keep targeted refcount diagnostics useful across ithreads. This is + // inert unless PJ_REFCOUNT_TRACE is enabled and a class was selected + // with PJ_REFCOUNT_TRACE_CLASS in its source runtime. + target.refCountTrace = source.refCountTrace; target.storedInPackageGlobal = source.storedInPackageGlobal; target.isPackageGlobalRoot = source.isPackageGlobalRoot; // Refcount/lifecycle queues are reconstructed in the child runtime. @@ -902,9 +1006,16 @@ private boolean isWeak(RuntimeScalar scalar) { private void finishWeakReferences() { if (weakReferences.isEmpty()) return; try (PerlRuntime.Binding ignored = targetRuntime.bind()) { + int pendingBeforeWeakEdges = MortalList.pendingSize(); for (RuntimeScalar weakReference : weakReferences) { WeakRefRegistry.weakenForSnapshot(weakReference, snapshotStrongRoots); } + // Cloned captures can temporarily contribute a strong bridge until + // their weak edges are installed. Retire those releases only after + // every snapshot weak edge is registered, so a DESTROY callback + // sees the complete weak graph and no stale capture owner survives + // into the child thread's first statement. + MortalList.drainPendingSince(pendingBeforeWeakEdges); } weakReferences.clear(); snapshotStrongRoots.clear(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java index d7ba5c74dd..3e84127061 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java @@ -152,13 +152,22 @@ Object snapshotRegexMutationState() { void restoreRegexMutationState(Object token) { if (!(token instanceof RegexMutationState state)) return; deferValuesAddedSince(state.elements.values()); + List replaced = new ArrayList<>(elements.values()); elements = newElementMap(state.elements); + for (RuntimeScalar value : replaced) { + if (value != null) value.reconcileContainerOwnerAfterMutation(this); + } + refreshUntrackedElementOwners(); byteKeys = state.byteKeys == null ? null : new HashSet<>(state.byteKeys); type = state.type; blessId = state.blessId; taintEnvironmentAliasDescription = state.taintEnvironmentAliasDescription; if (isPackageRootedHash()) { - for (RuntimeScalar value : elements.values()) markPackageRootedValue(value); + for (RuntimeScalar value : elements.values()) { + markPackageRootedValue(value); + value.markPrepopulatedPackageAggregateOwner(this); + value.refreshUntrackedReferenceOwner(); + } } } @@ -178,12 +187,22 @@ private void deferValuesAddedSince(Collection saved) { MortalList.deferDestroyForContainerClear(added); } + private void refreshUntrackedElementOwners() { + if (elements == null) return; + for (RuntimeScalar value : elements.values()) { + if (value != null) value.reconcileContainerOwnerAfterMutation(this); + } + } + private record RegexMutationState(Map elements, Set byteKeys, int type, int blessId, String taintEnvironmentAliasDescription) {} private static final class RuntimeHashElementMap extends StableHashMap { private final RuntimeHash owner; + private Set> entrySetView; + private Set keySetView; + private Collection valuesView; private RuntimeHashElementMap(RuntimeHash owner) { this.owner = owner; @@ -207,17 +226,333 @@ public RuntimeScalar put(String key, RuntimeScalar value) { } owner.notePackageRootMutation(previous, value); if (value != null) { - value.markContainerOwner(owner); if (owner.hasDurableElementLifetime()) { RuntimeScalar.retainUnstashedIoForDurableSlot(value); } } - owner.markPackageRootedValue(value); RuntimeScalar result = super.put(key, value); + if (value != null) value.markContainerOwner(owner); + owner.markPackageRootedValue(value); + refreshStoredUntrackedOwner(value); + if (previous != null && previous != value) { + previous.reconcileContainerOwnerAfterMutation(owner); + } updateReferentHashSlotOwnership(previous, value); return result; } + @Override + public RuntimeScalar putIfAbsent(String key, RuntimeScalar value) { + RuntimeScalar current = get(key); + return current == null ? put(key, value) : current; + } + + @Override + public boolean remove(Object key, Object value) { + if (!containsKey(key) || !Objects.equals(get(key), value)) return false; + remove(key); + return true; + } + + @Override + public boolean replace(String key, RuntimeScalar oldValue, RuntimeScalar newValue) { + if (!containsKey(key) || !Objects.equals(get(key), oldValue)) return false; + put(key, newValue); + return true; + } + + @Override + public RuntimeScalar replace(String key, RuntimeScalar value) { + if (!containsKey(key)) return null; + return put(key, value); + } + + @Override + public void replaceAll(java.util.function.BiFunction function) { + Objects.requireNonNull(function); + for (String key : new ArrayList<>(super.keySet())) { + put(key, function.apply(key, get(key))); + } + } + + @Override + public RuntimeScalar computeIfAbsent(String key, + java.util.function.Function function) { + Objects.requireNonNull(function); + RuntimeScalar current = get(key); + if (current != null) return current; + RuntimeScalar computed = function.apply(key); + if (computed != null) put(key, computed); + return computed; + } + + @Override + public RuntimeScalar computeIfPresent(String key, + java.util.function.BiFunction function) { + Objects.requireNonNull(function); + RuntimeScalar current = get(key); + if (current == null) return null; + RuntimeScalar computed = function.apply(key, current); + if (computed == null) remove(key); + else put(key, computed); + return computed; + } + + @Override + public RuntimeScalar compute(String key, + java.util.function.BiFunction function) { + Objects.requireNonNull(function); + RuntimeScalar current = get(key); + boolean present = current != null || containsKey(key); + RuntimeScalar computed = function.apply(key, current); + if (computed == null) { + if (present) remove(key); + } else { + put(key, computed); + } + return computed; + } + + @Override + public RuntimeScalar merge(String key, RuntimeScalar value, + java.util.function.BiFunction function) { + Objects.requireNonNull(value); + Objects.requireNonNull(function); + RuntimeScalar current = get(key); + RuntimeScalar merged = current == null ? value : function.apply(current, value); + if (merged == null) remove(key); + else put(key, merged); + return merged; + } + + @Override + public Set> entrySet() { + if (entrySetView == null) { + entrySetView = new AbstractSet<>() { + @Override + public int size() { + return RuntimeHashElementMap.this.size(); + } + + @Override + public void clear() { + RuntimeHashElementMap.this.clear(); + } + + @Override + public boolean contains(Object candidate) { + if (!(candidate instanceof Map.Entry entry)) return false; + Object key = entry.getKey(); + return RuntimeHashElementMap.this.containsKey(key) + && Objects.equals(RuntimeHashElementMap.this.get(key), entry.getValue()); + } + + @Override + public boolean remove(Object candidate) { + return candidate instanceof Map.Entry entry + && RuntimeHashElementMap.this.remove(entry.getKey(), entry.getValue()); + } + + @Override + public Iterator> iterator() { + return ownerAwareEntryIterator(); + } + }; + } + return entrySetView; + } + + @Override + public Set keySet() { + if (keySetView == null) { + keySetView = new AbstractSet<>() { + @Override + public int size() { + return RuntimeHashElementMap.this.size(); + } + + @Override + public boolean contains(Object key) { + return RuntimeHashElementMap.this.containsKey(key); + } + + @Override + public boolean remove(Object key) { + if (!RuntimeHashElementMap.this.containsKey(key)) return false; + RuntimeHashElementMap.this.remove(key); + return true; + } + + @Override + public void clear() { + RuntimeHashElementMap.this.clear(); + } + + @Override + public Iterator iterator() { + Iterator> delegate = + RuntimeHashElementMap.super.entrySet().iterator(); + return new Iterator<>() { + private Map.Entry currentEntry; + private boolean canRemove; + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public String next() { + currentEntry = delegate.next(); + canRemove = true; + return currentEntry.getKey(); + } + + @Override + public void remove() { + if (!canRemove) throw new IllegalStateException(); + RuntimeScalar removedValue = currentEntry.getValue(); + delegate.remove(); + retireRemovedValue(removedValue); + currentEntry = null; + canRemove = false; + } + }; + } + }; + } + return keySetView; + } + + @Override + public Collection values() { + if (valuesView == null) { + valuesView = new AbstractCollection<>() { + @Override + public int size() { + return RuntimeHashElementMap.this.size(); + } + + @Override + public void clear() { + RuntimeHashElementMap.this.clear(); + } + + @Override + public Iterator iterator() { + Iterator> delegate = + RuntimeHashElementMap.super.entrySet().iterator(); + return new Iterator<>() { + private Map.Entry currentEntry; + private RuntimeScalar currentValue; + private boolean canRemove; + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public RuntimeScalar next() { + currentEntry = delegate.next(); + currentValue = currentEntry.getValue(); + canRemove = true; + return currentValue; + } + + @Override + public void remove() { + if (!canRemove) throw new IllegalStateException(); + RuntimeScalar removedValue = currentEntry.getValue(); + delegate.remove(); + retireRemovedValue(removedValue); + currentEntry = null; + currentValue = null; + canRemove = false; + } + }; + } + }; + } + return valuesView; + } + + private Iterator> ownerAwareEntryIterator() { + Iterator> delegate = + RuntimeHashElementMap.super.entrySet().iterator(); + return new Iterator<>() { + private Map.Entry currentEntry; + private String currentKey; + private boolean canRemove; + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public Map.Entry next() { + Map.Entry entry = delegate.next(); + currentEntry = entry; + currentKey = entry.getKey(); + canRemove = true; + String key = currentKey; + return new Map.Entry<>() { + @Override + public String getKey() { + return key; + } + + @Override + public RuntimeScalar getValue() { + return RuntimeHashElementMap.this.get(key); + } + + @Override + public RuntimeScalar setValue(RuntimeScalar value) { + return RuntimeHashElementMap.this.put(key, value); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Map.Entry entry)) return false; + return Objects.equals(key, entry.getKey()) + && Objects.equals(getValue(), entry.getValue()); + } + + @Override + public int hashCode() { + return Objects.hashCode(key) ^ Objects.hashCode(getValue()); + } + }; + } + + @Override + public void remove() { + if (!canRemove) throw new IllegalStateException(); + RuntimeScalar removedValue = currentEntry.getValue(); + delegate.remove(); + retireRemovedValue(removedValue); + currentEntry = null; + currentKey = null; + canRemove = false; + } + }; + } + + private void retireRemovedValue(RuntimeScalar previous) { + if (previous == null) return; + if (owner.isHintHash && !owner.suppressHintHashLifecycleCleanup) { + MortalList.deferDestroyForContainerClear(java.util.Collections.singletonList(previous)); + } + owner.notePackageRootMutation(previous, null); + previous.reconcileContainerOwnerAfterMutation(owner); + } + @Override public void putAll(Map m) { if (owner.threadShared) { @@ -228,45 +563,66 @@ public void putAll(Map m) { return; } boolean hasReferenceEdge = false; + Set replacedValues = Collections.newSetFromMap( + new IdentityHashMap<>()); for (Map.Entry e : m.entrySet()) { - if (rootEdge(super.get(e.getKey())) || rootEdge(e.getValue())) { + RuntimeScalar previous = super.get(e.getKey()); + if (previous != null && previous != e.getValue()) replacedValues.add(previous); + if (rootEdge(previous) || rootEdge(e.getValue())) { hasReferenceEdge = true; - break; } } owner.notePackageRootMutationIf(hasReferenceEdge); for (RuntimeScalar value : m.values()) { if (owner.threadShared) SharedPerlStorage.publishBlessing(value); if (value != null) { - value.markContainerOwner(owner); if (owner.hasDurableElementLifetime()) { RuntimeScalar.retainUnstashedIoForDurableSlot(value); } } - owner.markPackageRootedValue(value); } super.putAll(m); + for (RuntimeScalar value : m.values()) { + if (value != null) value.markContainerOwner(owner); + owner.markPackageRootedValue(value); + refreshStoredUntrackedOwner(value); + } + for (RuntimeScalar value : replacedValues) { + value.reconcileContainerOwnerAfterMutation(owner); + } + } + + private void refreshStoredUntrackedOwner(RuntimeScalar value) { + // Bulk copy reconstructs a replacement map before assigning it to + // owner.elements. Refresh only once this map is installed. + if (value != null && owner.elements == this) { + if (owner.isPackageRootedHash()) { + value.markPrepopulatedPackageAggregateOwner(owner); + } + value.reconcileContainerOwnerAfterMutation(owner); + } } @Override public RuntimeScalar remove(Object key) { RuntimeScalar previous = super.remove(key); - if (previous != null) { - if (owner.isHintHash && !owner.suppressHintHashLifecycleCleanup) { - MortalList.deferDestroyForContainerClear(java.util.Collections.singletonList(previous)); - } - owner.notePackageRootMutation(previous, null); - } + retireRemovedValue(previous); return previous; } @Override public void clear() { if (!isEmpty()) { + List removed = new ArrayList<>(values()); if (owner.isHintHash && !owner.suppressHintHashLifecycleCleanup) { - MortalList.deferDestroyForContainerClear(values()); + MortalList.deferDestroyForContainerClear(removed); } - owner.notePackageRootClear(values()); + owner.notePackageRootClear(removed); + super.clear(); + for (RuntimeScalar scalar : removed) { + if (scalar != null) scalar.reconcileContainerOwnerAfterMutation(owner); + } + return; } super.clear(); } @@ -320,6 +676,7 @@ void notePackageRootMutation() { void markPackageRootedValue(RuntimeScalar value) { if (!isPackageRootedHash() || value == null) return; value.isPackageGlobalRoot = true; + value.markContainerOwner(this); if (value.value instanceof RuntimeBase rb) { rb.storedInPackageGlobal = true; } @@ -662,7 +1019,7 @@ && isAggregateClearAssignment(existing, value)) { if (value == null) existing.undefine(); else value.addToScalar(existing); } else { - elements.put(key, independentSlot(key, value)); + storeOwnedElement(key, existing, value); } } case AUTOVIVIFY_HASH -> { @@ -678,7 +1035,7 @@ && isAggregateClearAssignment(existing, value)) { if (value == null) existing.undefine(); else value.addToScalar(existing); } else { - elements.put(key, independentSlot(key, value)); + storeOwnedElement(key, existing, value); } } case TIED_HASH -> { @@ -691,6 +1048,23 @@ && isAggregateClearAssignment(existing, value)) { } } + private void storeOwnedElement(String key, RuntimeScalar previous, RuntimeScalar value) { + RuntimeScalar stored = independentSlot(key, value); + elements.put(key, stored); + RuntimeScalar.incrementRefCountForContainerStore(stored); + if (previous != null && previous != stored) { + if (PerlRuntime.currentOrNull() != null) { + MortalList.deferDestroyForContainerClear(Collections.singletonList(previous)); + } else if (previous.refCountOwned && previous.value instanceof RuntimeBase referent) { + // Host-side graph construction has no Perl callbacks or mortal + // queue, but replacing a field still retires its counted edge. + previous.refCountOwned = false; + referent.releaseActiveOwner(previous); + if (referent.refCount > 0) referent.refCount--; + } + } + } + private void warnHashIteratorInsertion(RuntimeScalar existing) { if (existing != null || hashIterator == null || hashIteratorInsertionWarningIssued) { return; @@ -1830,12 +2204,16 @@ public RuntimeHash undefine() { // parallel-load DBIC tests pushed t/cdbi/68 / t/96 / t/debug/core // past their 300s harness timeout. if (!hasDestroyableValues()) { + List removed = new ArrayList<>(this.elements.values()); MortalList.deferDestroyForContainerClear(this.elements.values()); if (this.type == PLAIN_HASH) { this.elements = newElementMap(); } else { this.elements.clear(); } + for (RuntimeScalar value : removed) { + if (value != null) value.reconcileContainerOwnerAfterMutation(this); + } this.byteKeys = null; hashIterator = null; MortalList.flush(); @@ -1855,6 +2233,7 @@ public RuntimeHash undefine() { String key = entry.getKey(); RuntimeScalar value = entry.getValue(); it.remove(); + if (value != null) value.reconcileContainerOwnerAfterMutation(this); notePackageRootMutation(value, null); if (this.byteKeys != null) this.byteKeys.remove(key); // Defer DESTROY for this one value, then flush so it runs now. @@ -1978,7 +2357,9 @@ public void dynamicRestoreState() { this.blessId = savedBlessId; } notePackageRootClear(this.elements.values()); - this.elements = previousState.elements; + this.elements = newElementMap(previousState.elements); + refreshUntrackedElementOwners(); + previousState.elements.clear(); notePackageRootClear(this.elements.values()); if (isPackageRootedHash()) { for (RuntimeScalar value : this.elements.values()) { @@ -2012,6 +2393,7 @@ public void dynamicResumeState(Object token) { this.blessId = activeState.blessId; this.byteKeys = activeState.byteKeys != null ? new HashSet<>(activeState.byteKeys) : null; this.elements.putAll(activeState.elements); + activeState.elements.clear(); } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java index a08f033257..82094826f0 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java @@ -171,6 +171,16 @@ public RuntimeList cloneScalars() { return result; } + RuntimeList copyToMortalReturnSlots() { + RuntimeList result = new RuntimeList(); + for (RuntimeBase value : elements) { + result.elements.add(value instanceof RuntimeScalar scalar + && !scalar.isMortalReturnSnapshot() + ? RuntimeScalar.copyToMortalReturnSlot(scalar) : value); + } + return result; + } + /** * Adds the elements of this list to another RuntimeList. * @@ -787,7 +797,8 @@ public RuntimeArray setFromList(RuntimeList value) { // leak +1 refCount per call on the discarded first arg — visible // in op/inccode.t "no leaks" tests #61, #63 (bug perl #92252). if (rhsValue != null && rhsValue.refCountOwned - && (rhsValue.type & RuntimeScalarType.REFERENCE_BIT) != 0 + && ((rhsValue.type & RuntimeScalarType.REFERENCE_BIT) != 0 + || rhsValue.type == RuntimeScalarType.GLOB) && rhsValue.value instanceof RuntimeBase base && base.refCount > 0) { base.releaseOwner(rhsValue, "RuntimeList materialized-copy undo (undef-target)"); base.releaseActiveOwner(rhsValue); @@ -811,7 +822,8 @@ public RuntimeArray setFromList(RuntimeList value) { // Array/hash targets take direct ownership of materialized copies, so only // scalar targets need this correction. if (assigned != null && assigned.refCountOwned - && (assigned.type & RuntimeScalarType.REFERENCE_BIT) != 0 + && ((assigned.type & RuntimeScalarType.REFERENCE_BIT) != 0 + || assigned.type == RuntimeScalarType.GLOB) && assigned.value instanceof RuntimeBase base && base.refCount > 0) { base.releaseOwner(assigned, "RuntimeList materialized-copy undo (scalar-target)"); base.releaseActiveOwner(assigned); @@ -837,7 +849,9 @@ public RuntimeArray setFromList(RuntimeList value) { // setFromList creates new copies via incrementRefCountForContainerStore; // the materialized rhs elements' increments are now redundant. for (RuntimeScalar r : remaining) { - if (r.refCountOwned && (r.type & RuntimeScalarType.REFERENCE_BIT) != 0 + if (r.refCountOwned + && ((r.type & RuntimeScalarType.REFERENCE_BIT) != 0 + || r.type == RuntimeScalarType.GLOB) && r.value instanceof RuntimeBase base && base.refCount > 0) { base.releaseOwner(r, "RuntimeList materialized-copy undo (array-target)"); base.releaseActiveOwner(r); @@ -872,7 +886,9 @@ public RuntimeArray setFromList(RuntimeList value) { // via addToArray -> setLarge) are now redundant and would leak since // nobody decrements them. for (RuntimeScalar r : remainingArr.elements) { - if (r.refCountOwned && (r.type & RuntimeScalarType.REFERENCE_BIT) != 0 + if (r.refCountOwned + && ((r.type & RuntimeScalarType.REFERENCE_BIT) != 0 + || r.type == RuntimeScalarType.GLOB) && r.value instanceof RuntimeBase base && base.refCount > 0) { base.releaseOwner(r, "RuntimeList materialized-copy undo (hash-target)"); base.releaseActiveOwner(r); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index 0ffffa56b9..098960b9c1 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -334,6 +334,10 @@ boolean copiedFromArgumentFrame(Object frame) { return frame != null && copiedFromArgumentFrame == frame; } + Object copiedFromArgumentFrameToken() { + return copiedFromArgumentFrame; + } + /** Clears a non-reference array slot after aggregate ownership is queued. */ void clearForArraySlotRemoval() { this.type = UNDEF; @@ -423,6 +427,11 @@ public int captureRefCountOwned() { // Last hash/array container that stored this scalar slot. Used only as a // narrow guard for live CODE refs whose captures must survive void discard. RuntimeBase containerOwner; + private final Set containerOwners = + Collections.newSetFromMap(new IdentityHashMap<>()); + /** Package aggregates that contained this value before becoming roots. */ + private final Set prepopulatedPackageAggregateOwners = + Collections.newSetFromMap(new IdentityHashMap<>()); /** Return the aggregate slot that directly owns this scalar, if any. */ public RuntimeBase getContainerOwner() { @@ -435,35 +444,113 @@ void markContainerOwner(RuntimeBase owner) { // transient argument or container alive as long as the cache entry. // Reference and proxy-backed scalars retain this edge for lifetime // tracking. - if (getClass() == RuntimeScalarReadOnly.class + if (isSharedImmutableLiteralWithoutOwnerTracking()) return; + synchronized (this) { + if (owner != null) containerOwners.add(owner); + this.containerOwner = owner; + } + propagateTiedHandlerMarkerToReferent(); + } + + private boolean isSharedImmutableLiteralWithoutOwnerTracking() { + return getClass() == RuntimeScalarReadOnly.class && type != RuntimeScalarType.READONLY_SCALAR && (type & RuntimeScalarType.REFERENCE_BIT) == 0 && !(value instanceof RuntimeBase) - && !firstClassRegexScalar) { - return; + && !firstClassRegexScalar; + } + + void markPrepopulatedPackageAggregateOwner(RuntimeBase owner) { + boolean tracked; + synchronized (this) { + tracked = containerOwners.contains(owner); + } + if (!tracked || !isStoredInContainer(owner)) return; + synchronized (this) { + if (containerOwners.contains(owner)) prepopulatedPackageAggregateOwners.add(owner); + } + } + + /** Retire this cell's container edge when the container loses its last Perl owner. */ + void releaseContainerOwner(RuntimeBase expectedOwner) { + removeContainerOwnerTracking(expectedOwner); + refreshUntrackedReferenceOwner(); + } + + /** Retire this aggregate edge after a slot mutation removed its last copy. */ + void reconcileContainerOwnerAfterMutation(RuntimeBase owner) { + if (owner == null) return; + if (isSharedImmutableLiteralWithoutOwnerTracking()) return; + if (isStoredInContainer(owner)) { + synchronized (this) { + containerOwners.add(owner); + containerOwner = owner; + } + propagateTiedHandlerMarkerToReferent(); + } else { + removeContainerOwnerTracking(owner); + } + refreshUntrackedReferenceOwner(); + } + + private void removeContainerOwnerTracking(RuntimeBase expectedOwner) { + boolean replaceCurrentOwner; + synchronized (this) { + containerOwners.remove(expectedOwner); + prepopulatedPackageAggregateOwners.remove(expectedOwner); + replaceCurrentOwner = containerOwner == expectedOwner; + if (replaceCurrentOwner) containerOwner = null; + } + if (replaceCurrentOwner) { + RuntimeBase replacement = containerOwnerSnapshot().stream() + .filter(this::isStoredInContainer) + .findFirst().orElse(null); + synchronized (this) { + // A concurrent store may already have installed a newer owner. + if (containerOwner == null) containerOwner = replacement; + } + } + } + + /** Reconcile the fast owner hint with all currently stored container edges. */ + void reconcileContainerOwners() { + for (RuntimeBase owner : containerOwnerSnapshot()) { + if (!isStoredInContainer(owner)) removeContainerOwnerTracking(owner); + } + List owners = containerOwnerSnapshot(); + RuntimeBase current = containerOwner; + if (current != null && (!owners.contains(current) || !isStoredInContainer(current))) { + synchronized (this) { + if (containerOwner == current) containerOwner = null; + } + } + if (containerOwner == null) { + RuntimeBase replacement = owners.stream() + .filter(this::isStoredInContainer) + .findFirst().orElse(null); + synchronized (this) { + if (containerOwner == null) containerOwner = replacement; + } } - this.containerOwner = owner; - propagateTiedHandlerMarkerToReferent(); } private void propagateTiedHandlerMarkerToReferent() { - if (containerOwner != null && containerOwner.possiblyStoredInTiedHandler - && (type & RuntimeScalarType.REFERENCE_BIT) != 0 - && value instanceof RuntimeBase base) { + RuntimeBase owner; + RuntimeBase base; + synchronized (this) { + owner = containerOwner; + base = (type & RuntimeScalarType.REFERENCE_BIT) != 0 + && value instanceof RuntimeBase referent ? referent : null; + } + if (owner != null && owner.possiblyStoredInTiedHandler && base != null) { base.markPossiblyStoredInTiedHandler(); } } boolean isStoredInRegisteredContainerOwner() { - RuntimeBase owner = containerOwner; - if (owner == null || !MyVarCleanupStack.isRegistered(owner)) return false; - if (owner instanceof RuntimeHash hash) { - return hash.elements.containsValue(this); - } - if (owner instanceof RuntimeArray array) { - return array.elements.contains(this); - } - return false; + return containerOwnerSnapshot().stream() + .anyMatch(owner -> MyVarCleanupStack.isRegistered(owner) + && isStoredInAnyContainer(owner)); } /** Whether this scalar is backed by durable Perl storage for lvalue use. */ @@ -477,21 +564,17 @@ boolean hasDurableLvalueStorage() { /** True when a container removal returned this scalar slot as a loose value. */ private boolean isDetachedFromContainerOwner() { - RuntimeBase owner = containerOwner; - if (owner == null) { + List owners = containerOwnerSnapshot(); + if (owners.stream().anyMatch(this::isStoredInAnyContainer)) return false; + if (owners.isEmpty()) { return !MyVarCleanupStack.isRegistered(this) && !isPackageGlobalRoot; } - if (owner instanceof RuntimeHash hash) { - return !hash.elements.containsValue(this); - } - if (owner instanceof RuntimeArray array) { - return !array.elements.contains(this); - } - return false; + return true; } public void retainClosureCapture() { boolean firstCapture = captureCount++ == 0; + refreshUntrackedReferenceOwner(); if (firstCapture && type == RuntimeScalarType.CODE) { retainClosureCaptureReferent(); } else if (firstCapture && !refCountOwned) { @@ -512,13 +595,39 @@ public void retainClosureCapture() { } } + /** Retain this lexical cell for one closure capture with a distinct owner identity. */ + public PerlOwnerSlot retainClosureCaptureSlot() { + return retainClosureCaptureSlot(null); + } + + /** Retain this lexical cell and identify the CODE object that owns the capture. */ + public PerlOwnerSlot retainClosureCaptureSlot(RuntimeCode captureOwner) { + retainClosureCapture(); + PerlOwnerSlot ownerSlot = new PerlOwnerSlot(PerlOwnerSlot.Kind.SCALAR_CAPTURE); + ownerSlot.acquireCaptureIdentity(this, captureOwner); + return ownerSlot; + } + + /** Release one closure's lexical-cell owner and its matching capture count. */ + public void releaseClosureCaptureSlot(PerlOwnerSlot ownerSlot) { + if (ownerSlot != null) { + if (!ownerSlot.isActive()) return; + ownerSlot.release(); + } + releaseClosureCapture(); + } + /** Reconstruct capture ownership for an independent ithread snapshot. */ - void retainThreadCloneClosureCapture() { + PerlOwnerSlot retainThreadCloneClosureCapture(RuntimeCode captureOwner) { captureCount++; + refreshUntrackedReferenceOwner(); // An owning lexical slot already protects its referent. Borrowed // argument aliases do not, and the source runtime's other owners are // not a lifetime guarantee in the cloned runtime. if (!refCountOwned) retainClosureCaptureReferent(); + PerlOwnerSlot ownerSlot = new PerlOwnerSlot(PerlOwnerSlot.Kind.SCALAR_CAPTURE); + ownerSlot.acquireCaptureIdentity(this, captureOwner); + return ownerSlot; } public void releaseClosureCapture() { @@ -528,6 +637,10 @@ public void releaseClosureCapture() { if (captureCount == 0) { releaseOneClosureCaptureReferent(); releaseClosureOwnerSlot(); + if (scopeExited) { + cleanupScalarReferenceBinding(this); + } + refreshUntrackedReferenceOwner(); } } @@ -659,6 +772,514 @@ void retainClosureCaptureReferentsForUnweaken() { */ public boolean refCountOwned; + private PerlOwnerSlot countedOwnerSlot; + /** Identity for this scalar cell's strong edge to a non-counted referent. */ + private PerlOwnerSlot untrackedReferenceOwnerSlot; + private ArrayList argumentAliasOwnerSlots; + // Names sharing one GP/CODE scalar share one CV edge. A saved localized + // slot also keeps this binding alive while its names address a new slot. + private int codeSlotBindings; + + boolean hasCodeSlotBinding() { + return codeSlotBindings != 0; + } + + void retainCodeSlotBinding() { + codeSlotBindings++; + refreshCodeSlotOwner(); + } + + void releaseCodeSlotBinding() { + if (codeSlotBindings <= 0) throw new IllegalStateException("unbalanced CODE slot binding"); + if (--codeSlotBindings != 0) return; + refreshUntrackedReferenceOwner(); + MortalList.deferDecrementIfTracked(this); + MortalList.flushAboveMark(); + } + + private void prepareCodeSlotValue(RuntimeScalar source) { + if (codeSlotBindings == 0 || source.type != RuntimeScalarType.CODE + || !(source.value instanceof RuntimeCode code)) return; + code.hasCountedCodeSlots = true; + code.hadStashRef = true; + if (code.refCount == -1) code.refCount = 0; + } + + void refreshCodeSlotOwner() { + if (codeSlotBindings == 0 || type != RuntimeScalarType.CODE + || !(value instanceof RuntimeCode code)) return; + MortalList.setActive(true); + prepareCodeSlotValue(this); + incrementRefCountForContainerStore(this); + if (refCountOwned) { + countedOwnerSlot.reclassifyCountedIdentity(PerlOwnerSlot.Kind.CODE_SLOT); + } + } + private boolean mortalReturnSnapshot; + private boolean mortalReturnValueOwner; + private boolean tiedHandlerValueOwner; + private boolean stagedValueOwner; + + boolean isMortalReturnSnapshot() { + return mortalReturnSnapshot && !hasStorageOwnerForVoidDiscard() + && !RuntimeCode.isCurrentArgumentAlias(this); + } + + /** Identity of this scalar cell's counted edge, retained across assignments. */ + PerlOwnerSlot countedOwnerSlot() { + return countedOwnerSlot; + } + + PerlOwnerSlot untrackedReferenceOwnerSlot() { + return untrackedReferenceOwnerSlot; + } + + private boolean hasDurableStrongValueSlot() { + return hasDurableStrongValueSlot(false); + } + + /** Whether a slot is attached to a container that itself still has a Perl owner. */ + boolean hasLiveDurableStrongValueSlot() { + return hasDurableStrongValueSlot(true); + } + + private boolean hasDurableStrongValueSlot(boolean requireLiveContainer) { + PerlRuntime runtime = PerlRuntime.currentOrNull(); + boolean registeredLexical = runtime != null && MyVarCleanupStack.isRegistered(this); + boolean liveOwningContainer = false; + for (RuntimeBase owner : containerOwnerSnapshot()) { + if (!isStoredInContainer(owner)) { + removeContainerOwnerTracking(owner); + continue; + } + boolean liveContainerOwner = owner.hasPerlStrongOwner() + || (runtime != null && MyVarCleanupStack.isRegistered(owner)); + if (!requireLiveContainer || liveContainerOwner) liveOwningContainer = true; + } + return liveOwningContainer + || ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_SCALAR_SLOT) != 0 + || localBindingExists + || hasCodeSlotBinding() + || captureCount > 0 + || registeredLexical + || mortalReturnValueOwner + || tiedHandlerValueOwner + || stagedValueOwner + || (referencedByScalarReference + && (hasScalarCellOwner() || (!requireLiveContainer && refCount > 0))) + || this instanceof RuntimeBaseProxy; + } + + private boolean isStoredInContainer(RuntimeBase owner) { + return owner instanceof RuntimeHash hash + ? hash.elements != null && hash.elements.containsValue(this) + : owner instanceof RuntimeArray array && !array.elementsAliased + && array.elements != null && array.elements.contains(this); + } + + private boolean isStoredInAnyContainer(RuntimeBase owner) { + return owner instanceof RuntimeHash hash + ? hash.elements != null && hash.elements.containsValue(this) + : owner instanceof RuntimeArray array + && array.elements != null && array.elements.contains(this); + } + + private boolean hasStoredContainerOwnerWithoutCurrentOwnerMarker() { + if (containerOwner != null) return false; + for (RuntimeBase owner : containerOwnerSnapshot()) { + if (isStoredInAnyContainer(owner)) return true; + } + return false; + } + + private synchronized List containerOwnerSnapshot() { + return new ArrayList<>(containerOwners); + } + + private synchronized List prepopulatedPackageAggregateOwnerSnapshot() { + return new ArrayList<>(prepopulatedPackageAggregateOwners); + } + + boolean isPackageAggregateUntrackedReferenceOwner() { + if (type != RuntimeScalarType.ARRAYREFERENCE + && type != RuntimeScalarType.HASHREFERENCE) return false; + RuntimeBase directOwner = containerOwner; + return directOwner != null && hasPackageAggregateOwner(); + } + + boolean hasPackageAggregateUntrackedReferenceOwner() { + if (type != RuntimeScalarType.ARRAYREFERENCE + && type != RuntimeScalarType.HASHREFERENCE) return false; + return hasPackageAggregateOwner(); + } + + private boolean hasPackageAggregateOwner() { + for (RuntimeBase owner : containerOwnerSnapshot()) { + if (isStoredInContainer(owner) + && GlobalVariable.isPackageRootedAggregate(owner)) { + return true; + } + } + return false; + } + + private boolean hasScalarCellOwnerOtherThanClosureCapture() { + if (!referencedByScalarReference) return false; + if (localBindingExists || hasPendingReturnOwner()) return true; + for (PerlOwnerSlot slot : ownerSlotSnapshot()) { + if (slot.isActive() + && slot.kind() != PerlOwnerSlot.Kind.SCALAR_CAPTURE + && slot.kind() != PerlOwnerSlot.Kind.TEMPORARY_ROOT) { + return true; + } + } + return false; + } + + private boolean hasDirectScalarCellStorageOwner() { + if (localBindingExists || MyVarCleanupStack.isRegistered(this) + || ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_SCALAR_SLOT) != 0 + || hasCodeSlotBinding() || this instanceof RuntimeBaseProxy + || isStoredInRegisteredContainerOwner()) { + return true; + } + for (RuntimeBase owner : containerOwnerSnapshot()) { + if (owner instanceof RuntimeHash hash && hash.isPackageRootedHash() + && hash.elements != null && hash.elements.containsValue(this)) { + return true; + } + if (owner instanceof RuntimeArray array && array.isPackageGlobalRoot + && array.elements != null && array.elements.contains(this)) { + return true; + } + } + return referencedByScalarReference && (refCount > 0 + || hasScalarCellOwnerOtherThanClosureCapture()); + } + + private void retainUntrackedReferenceOwner(RuntimeBase referent) { + if (untrackedReferenceOwnerSlot == null) { + untrackedReferenceOwnerSlot = new PerlOwnerSlot( + PerlOwnerSlot.Kind.UNTRACKED_REFERENCE); + untrackedReferenceOwnerSlot.associateUntrackedScalarOwner(this); + } else if (untrackedReferenceOwnerSlot.referent() != referent) { + releaseUntrackedReferenceOwner(untrackedReferenceOwnerSlot.referent()); + } + untrackedReferenceOwnerSlot.acquireUntrackedIdentity(referent); + } + + void releaseUntrackedReferenceOwner(RuntimeBase expectedReferent) { + if (untrackedReferenceOwnerSlot == null + || untrackedReferenceOwnerSlot.referent() != expectedReferent) return; + untrackedReferenceOwnerSlot.release(); + WeakRefRegistry.onStrongOwnerSlotReleased(expectedReferent); + } + + void finalizeUntrackedValueAfterScopeExit(RuntimeBase expectedReferent) { + if (expectedReferent == null) return; + refreshUntrackedReferenceOwner(); + if (hasDurableStrongValueSlot()) return; + releaseUntrackedReferenceOwner(expectedReferent); + WeakRefRegistry.finalizeUntrackedAfterOwnerScopeExit(expectedReferent); + } + + private void updateUntrackedReferenceOwner(RuntimeBase referent) { + // RuntimeGraphCloner may rebuild a tied handler before it binds the + // destination runtime. Such a fresh scalar cannot be a registered weak + // observer yet, and its owner identity still belongs on the clone. + PerlRuntime runtime = PerlRuntime.currentOrNull(); + boolean weakValue = runtime != null && WeakRefRegistry.isweak(this); + boolean existingOwnerForReferent = untrackedReferenceOwnerSlot != null + && untrackedReferenceOwnerSlot.isActive() + && untrackedReferenceOwnerSlot.referent() == referent; + boolean countedOwnerActive = refCountOwned + && countedOwnerSlot != null + && countedOwnerSlot.isActive() + && countedOwnerSlot.referent() == referent; + boolean packageScalarValueOwner = + ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_SCALAR_SLOT) != 0; + boolean packageAggregateValueOwner = referent != null + && prepopulatedPackageAggregateOwnerSnapshot().stream().anyMatch(owner -> + isStoredInContainer(owner) + && GlobalVariable.isPackageRootedAggregate(owner)); + boolean localAggregateValueOwner = containerOwnerSnapshot().stream().anyMatch(owner -> + (owner instanceof RuntimeHash || owner instanceof RuntimeArray) + && !GlobalVariable.isPackageRootedAggregate(owner)); + boolean liveAggregateValueOwner = localAggregateValueOwner + && hasLiveDurableStrongValueSlot(); + boolean untrackedStrongValue = !weakValue + && referent != null + && referent.refCount != Integer.MIN_VALUE + && (referent.refCount < 0 + || existingOwnerForReferent + || packageScalarValueOwner + || packageAggregateValueOwner + || liveAggregateValueOwner) + && !countedOwnerActive; + if (hasDurableStrongValueSlot() && untrackedStrongValue) { + retainUntrackedReferenceOwner(referent); + } else if (untrackedReferenceOwnerSlot != null) { + releaseUntrackedReferenceOwner(untrackedReferenceOwnerSlot.referent()); + } + } + + void refreshUntrackedReferenceOwner() { + updateUntrackedReferenceOwner((type & RuntimeScalarType.REFERENCE_BIT) != 0 + && value instanceof RuntimeBase referent ? referent : null); + } + + static final class ArgumentAliasOwner { + private final RuntimeScalar scalar; + private final PerlOwnerSlot ownerSlot; + + private ArgumentAliasOwner(RuntimeScalar scalar, PerlOwnerSlot ownerSlot) { + this.scalar = scalar; + this.ownerSlot = ownerSlot; + } + + boolean matches(RuntimeScalar candidate) { + return scalar == candidate; + } + + void release() { + scalar.releaseArgumentAliasOwner(ownerSlot); + } + } + + synchronized ArgumentAliasOwner retainArgumentAliasOwner() { + PerlOwnerSlot ownerSlot = new PerlOwnerSlot(PerlOwnerSlot.Kind.ARGUMENT_ALIAS); + if (argumentAliasOwnerSlots == null) argumentAliasOwnerSlots = new ArrayList<>(); + argumentAliasOwnerSlots.add(ownerSlot); + refreshArgumentAliasOwnerSlots(); + return new ArgumentAliasOwner(this, ownerSlot); + } + + private void releaseArgumentAliasOwner(PerlOwnerSlot ownerSlot) { + synchronized (this) { + if (argumentAliasOwnerSlots != null) { + argumentAliasOwnerSlots.remove(ownerSlot); + if (argumentAliasOwnerSlots.isEmpty()) argumentAliasOwnerSlots = null; + } + } + ownerSlot.release(); + } + + synchronized void refreshArgumentAliasOwnerSlots() { + if (argumentAliasOwnerSlots == null) return; + RuntimeBase referent = null; + if (((type & RuntimeScalarType.REFERENCE_BIT) != 0 || type == RuntimeScalarType.GLOB) + && value instanceof RuntimeBase base + && !WeakRefRegistry.isweak(this)) { + referent = base; + } + for (PerlOwnerSlot ownerSlot : argumentAliasOwnerSlots) { + if (referent == null) ownerSlot.release(); + else ownerSlot.acquireArgumentAliasIdentity(referent); + } + } + + boolean hasStorageOwnerForVoidDiscard() { + if (captureCount > 0 || isPackageGlobalRoot || MyVarCleanupStack.isRegistered(this)) { + return true; + } + return countedOwnerSlot != null && countedOwnerSlot.isActive() + && (countedOwnerSlot.kind() == PerlOwnerSlot.Kind.ARRAY_ELEMENT + || countedOwnerSlot.kind() == PerlOwnerSlot.Kind.HASH_ELEMENT); + } + + /** Whether this scalar is a durable Perl cell that can be aliased by a call. */ + boolean hasPersistentArgumentStorageCell() { + if (captureCount > 0 || isPackageGlobalRoot || localBindingExists + || MyVarCleanupStack.isRegistered(this)) { + return true; + } + if (containerOwner instanceof RuntimeHash) return true; + return containerOwner instanceof RuntimeArray array && !array.elementsAliased; + } + + void recordCountedOwner(RuntimeBase referent) { + if (countedOwnerSlot == null) { + countedOwnerSlot = new PerlOwnerSlot(countedOwnerKind()); + } + countedOwnerSlot.acquireCountedIdentity(referent); + countedOwnerSlot.associateCountedScalarOwner(this); + } + + boolean isCurrentAggregateElementOwner(RuntimeBase referent, PerlOwnerSlot slot) { + if (countedOwnerSlot != slot || !refCountOwned || value != referent + || !slot.isActive() || slot.referent() != referent) { + return false; + } + if (slot.kind() == PerlOwnerSlot.Kind.ARRAY_ELEMENT) { + return containerOwner instanceof RuntimeArray array + && !array.elementsAliased && array.elements.contains(this); + } + return slot.kind() == PerlOwnerSlot.Kind.HASH_ELEMENT + && containerOwner instanceof RuntimeHash hash + && hash.elements.containsValue(this); + } + + boolean isCurrentCountedScalarOwner(RuntimeBase referent, PerlOwnerSlot slot) { + PerlOwnerSlot.Kind kind = slot.kind(); + if ((kind != PerlOwnerSlot.Kind.SCALAR_CELL + && kind != PerlOwnerSlot.Kind.CODE_SLOT + && kind != PerlOwnerSlot.Kind.ARGUMENT_VALUE + && kind != PerlOwnerSlot.Kind.RETURN_VALUE + && kind != PerlOwnerSlot.Kind.TIE_HANDLER) + || countedOwnerSlot != slot || !refCountOwned || value != referent + || ((type & RuntimeScalarType.REFERENCE_BIT) == 0 + && type != RuntimeScalarType.GLOB + && type != RuntimeScalarType.CODE) + || WeakRefRegistry.isweak(this) || !slot.isActive() + || slot.referent() != referent) { + return false; + } + if (kind == PerlOwnerSlot.Kind.SCALAR_CELL + && hasStoredContainerOwnerWithoutCurrentOwnerMarker() + && !localBindingExists + && !MyVarCleanupStack.isRegistered(this) + && ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_SCALAR_SLOT) == 0 + && !hasCodeSlotBinding() + && !(this instanceof RuntimeBaseProxy)) { + return false; + } + if (kind != PerlOwnerSlot.Kind.SCALAR_CELL + || hasDirectScalarCellStorageOwner()) { + return true; + } + return isCurrentCapturedScalarCellOwner(referent, slot); + } + + boolean isCurrentCapturedScalarCellOwner(RuntimeBase referent, PerlOwnerSlot slot) { + if (slot.kind() != PerlOwnerSlot.Kind.SCALAR_CELL + || countedOwnerSlot != slot || !refCountOwned || value != referent + || ((type & RuntimeScalarType.REFERENCE_BIT) == 0 + && type != RuntimeScalarType.GLOB + && type != RuntimeScalarType.CODE) + || WeakRefRegistry.isweak(this) || !slot.isActive() + || slot.referent() != referent || captureCount == 0) { + return false; + } + for (PerlOwnerSlot captureSlot : ownerSlotSnapshot()) { + if (captureSlot.kind() != PerlOwnerSlot.Kind.SCALAR_CAPTURE + || !captureSlot.isActive() || captureSlot.referent() != this) continue; + RuntimeCode captureOwner = captureSlot.captureOwner(); + if (captureOwner != null && captureOwner.hasPerlOwnerForCapture()) return true; + } + return false; + } + + private PerlOwnerSlot.Kind countedOwnerKind() { + return codeSlotBindings != 0 ? PerlOwnerSlot.Kind.CODE_SLOT + : containerOwner instanceof RuntimeHash ? PerlOwnerSlot.Kind.HASH_ELEMENT + : containerOwner instanceof RuntimeArray array + && !array.elementsAliased + ? PerlOwnerSlot.Kind.ARRAY_ELEMENT : PerlOwnerSlot.Kind.SCALAR_CELL; + } + + private void recordContainerCountedOwner() { + if (!refCountOwned || !(value instanceof RuntimeBase referent)) return; + recordCountedOwner(referent); + countedOwnerSlot.reclassifyCountedIdentity(countedOwnerKind()); + } + + static RuntimeScalar copyToMortalReturnSlot(RuntimeScalar source) { + RuntimeScalar copy = new RuntimeScalar(source); + copy.mortalReturnSnapshot = true; + copy.copiedFromArgumentFrame = null; + incrementRefCountForContainerStore(copy); + if (copy.refCountOwned) { + copy.countedOwnerSlot.reclassifyCountedIdentity(PerlOwnerSlot.Kind.RETURN_VALUE); + MortalList.deferDecrementIfTracked(copy); + } else { + copy.mortalReturnValueOwner = true; + copy.refreshUntrackedReferenceOwner(); + if (copy.untrackedReferenceOwnerSlot != null) { + MortalList.deferUntrackedReturnValueOwnerRelease(copy); + } else { + copy.mortalReturnValueOwner = false; + } + } + return copy; + } + + void releaseMortalReturnValueOwner() { + if (!mortalReturnValueOwner) return; + mortalReturnValueOwner = false; + refreshUntrackedReferenceOwner(); + } + + /** Snapshot one tie wrapper's strong handler edge independently of its source. */ + static RuntimeScalar retainTiedHandlerValue(RuntimeScalar source) { + if (source == null) return null; + RuntimeScalar owner = new RuntimeScalar(source); + incrementRefCountForContainerStore(owner); + owner.tiedHandlerValueOwner = true; + owner.refreshUntrackedReferenceOwner(); + if (owner.refCountOwned) { + owner.countedOwnerSlot.reclassifyCountedIdentity(PerlOwnerSlot.Kind.TIE_HANDLER); + } + return owner; + } + + /** Retire the tie owner while retaining a borrow for deferred magic restoration. */ + static void releaseTiedHandlerValue(RuntimeScalar owner) { + if (owner == null || !owner.tiedHandlerValueOwner) return; + owner.tiedHandlerValueOwner = false; + owner.refreshUntrackedReferenceOwner(); + if (owner.refCountOwned) MortalList.deferDecrementIfTracked(owner); + if (PerlRuntime.currentOrNull() != null) MortalList.flushAboveMark(); + } + + static RuntimeScalar retainEvalReturnValue(RuntimeScalar source) { + RuntimeScalar owner = retainStagedArgumentValue(source); + if (owner.refCountOwned) { + owner.countedOwnerSlot.reclassifyCountedIdentity(PerlOwnerSlot.Kind.RETURN_VALUE); + } + return owner; + } + + static RuntimeScalar retainStagedArgumentValue(RuntimeScalar source) { + RuntimeScalar owner = new RuntimeScalar(); + owner.set(source); + owner.stagedValueOwner = true; + owner.refreshUntrackedReferenceOwner(); + if (owner.refCountOwned) { + owner.countedOwnerSlot.reclassifyCountedIdentity(PerlOwnerSlot.Kind.ARGUMENT_VALUE); + } + return owner; + } + + /** Release a temporary call/return owner after its boundary has drained. */ + static void releaseStagedArgumentValue(RuntimeScalar owner) { + if (owner == null || !owner.stagedValueOwner) return; + owner.stagedValueOwner = false; + owner.refreshUntrackedReferenceOwner(); + MortalList.deferDecrementIfTracked(owner); + } + + void releaseCountedOwner(RuntimeBase referent) { + if (countedOwnerSlot != null && countedOwnerSlot.referent() == referent) { + countedOwnerSlot.release(); + } + } + + PerlOwnerSlot.PendingRelease deferCountedOwnerRelease(RuntimeBase referent) { + return countedOwnerSlot != null && countedOwnerSlot.referent() == referent + ? countedOwnerSlot.releaseCountedIdentity() : null; + } + + private void transferCountedOwnerTo(RuntimeScalar destination) { + destination.countedOwnerSlot = countedOwnerSlot; + countedOwnerSlot = null; + if (destination.countedOwnerSlot != null) { + destination.countedOwnerSlot.associateCountedScalarOwner(destination); + } + if (destination.countedOwnerSlot == null && destination.value instanceof RuntimeBase referent) { + destination.recordCountedOwner(referent); + } + } + /** A postfix ++/-- result transfers, rather than duplicates, its reference owner on assignment. */ private boolean postfixReferenceOwnershipTransfer; @@ -1767,6 +2388,14 @@ public RuntimeArray setArrayOfAlias(RuntimeArray arr) { && !isPackageGlobalRoot && !localBindingExists && (code.refCount == 0 || scopeExited) && (code.capturedScalars != null || code.capturedAggregates != null)) { + // A temporary closure in @_ is a strong argument slot. Give the + // aliased cell its own counted identity so argument cleanup can + // release the captures from that count, without proving liveness + // by walking the Java object graph. + incrementRefCountForContainerStore(this); + if (countedOwnerSlot != null && countedOwnerSlot.isActive()) { + countedOwnerSlot.reclassifyCountedIdentity(PerlOwnerSlot.Kind.ARGUMENT_VALUE); + } arr.markOwnedAliasElement(this); } return arr; @@ -2032,6 +2661,7 @@ public RuntimeScalar addToScalar(RuntimeScalar scalar) { // set() acquired the destination owner; cancel the extra // increment because this temporary's owner was transferred. base.refCount--; + base.releaseActiveOwner(this); } } return result; @@ -2114,14 +2744,26 @@ public void vivifyLogicalAssignmentLvalue() { * constructor, and their refCount was already incremented at creation time. */ public static void incrementRefCountForContainerStore(RuntimeScalar scalar) { - if (scalar == null || scalar.refCountOwned - || (scalar.type & REFERENCE_BIT) == 0 + if (scalar == null) return; + if (PerlRuntime.currentOrNull() != null && WeakRefRegistry.isweak(scalar)) return; + if (scalar.refCountOwned) { + scalar.recordContainerCountedOwner(); + if (scalar.untrackedReferenceOwnerSlot != null) { + scalar.releaseUntrackedReferenceOwner( + scalar.untrackedReferenceOwnerSlot.referent()); + } + return; + } + if ((scalar.type & REFERENCE_BIT) == 0 || !(scalar.value instanceof RuntimeBase base)) { return; } // Destroyed / weaken sentinel states must not be reactivated here. - if (base.refCount == Integer.MIN_VALUE - || base.refCount == WeakRefRegistry.WEAKLY_TRACKED) { + if (base.refCount == Integer.MIN_VALUE) { + return; + } + if (base.refCount == WeakRefRegistry.WEAKLY_TRACKED) { + scalar.refreshUntrackedReferenceOwner(); return; } // Anonymous nested containers start at refCount=-1 (untracked). @@ -2133,6 +2775,7 @@ public static void incrementRefCountForContainerStore(RuntimeScalar scalar) { // (cpan Moo t/accessor-weaken.t). if (base.refCount < 0) { if (!(base instanceof RuntimeHash) && !(base instanceof RuntimeArray)) { + scalar.refreshUntrackedReferenceOwner(); return; } base.refCount = 0; @@ -2143,10 +2786,15 @@ public static void incrementRefCountForContainerStore(RuntimeScalar scalar) { base.recordOwner(scalar, "incrementRefCountForContainerStore"); base.recordActiveOwner(scalar); scalar.refCountOwned = true; + scalar.recordContainerCountedOwner(); + if (scalar.untrackedReferenceOwnerSlot != null) { + scalar.releaseUntrackedReferenceOwner( + scalar.untrackedReferenceOwnerSlot.referent()); + } // Phase B1 (refcount_alignment_52leaks_plan.md): track the // container element so ReachabilityWalker can see it via // ScalarRefRegistry. - ScalarRefRegistry.registerRef(scalar); + if (PerlRuntime.currentOrNull() != null) ScalarRefRegistry.registerRef(scalar); } public static void retainUnstashedIoForDurableSlot(RuntimeScalar scalar) { @@ -2177,7 +2825,7 @@ static RuntimeScalar copyReturnedIoAlias(RuntimeScalar source) { source.ioOwner = true; } copy.copiedFromArgumentFrame = null; - copy.containerOwner = null; + copy.reconcileContainerOwners(); return copy; } @@ -2282,6 +2930,7 @@ private RuntimeScalar setPlainInteger(RuntimeScalar value) { growingString = null; type = value.type; this.value = copyScalarPayload(value.type, value.value); + refreshArgumentAliasOwnerSlots(); utf8UncheckedOctets = value.utf8UncheckedOctets; utf8MalformedWarning = value.utf8MalformedWarning; tainted = value.tainted; @@ -2326,6 +2975,7 @@ public RuntimeScalar setArithmeticIntegerResult(long result, boolean resultTaint value = new MutableIntegerValue(result); } type = RuntimeScalarType.INTEGER; + refreshArgumentAliasOwnerSlots(); utf8UncheckedOctets = false; utf8MalformedWarning = null; tainted = resultTainted; @@ -2352,6 +3002,7 @@ private RuntimeScalar setSlow(RuntimeScalar value) { hash.preallocateCapacity(value.getInt()); this.type = RuntimeScalarType.INTEGER; this.value = hash.keys(RuntimeContextType.SCALAR).scalar().getInt(); + refreshArgumentAliasOwnerSlots(); return this; } throw new PerlCompilerException("Can't modify keys on array in list assignment"); @@ -2429,6 +3080,7 @@ private RuntimeScalar setSlow(RuntimeScalar value) { this.formatPictureTainted = value.formatPictureTainted; this.globalCodeRefFqn = value.globalCodeRefFqn; } + refreshArgumentAliasOwnerSlots(); refreshSubstrLvalues(); notifyModifiedWatchers(); return this; @@ -2521,6 +3173,7 @@ private RuntimeScalar setLarge(RuntimeScalar value) { closeIOOnDrop(); this.type = RuntimeScalarType.UNDEF; this.value = null; + refreshArgumentAliasOwnerSlots(); this.tainted = false; this.numericLiteralText = null; this.numericContextSeen = false; @@ -2573,6 +3226,7 @@ private RuntimeScalar setLarge(RuntimeScalar value) { // so no refCount was incremented/decremented, and no mortal entries were added. this.type = value.type; this.value = copyScalarPayload(value.type, value.value); + refreshArgumentAliasOwnerSlots(); this.utf8UncheckedOctets = value.utf8UncheckedOctets; this.utf8MalformedWarning = value.utf8MalformedWarning; this.tainted = value.tainted; @@ -2593,6 +3247,7 @@ private RuntimeScalar setLarge(RuntimeScalar value) { * Separated to keep setLarge() small enough for JIT inlining of set(). */ private RuntimeScalar setLargeRefCounted(RuntimeScalar value) { + prepareCodeSlotValue(value); String packageCodeRefName = isPackageGlobalRoot ? globalCodeRefFqn : null; if (isPackageGlobalRoot) { MortalList.invalidateExternalRootSnapshot(); @@ -2638,11 +3293,15 @@ private RuntimeScalar setLargeRefCounted(RuntimeScalar value) { } else { this.type = value.type; this.value = copyScalarPayload(value.type, value.value); + refreshArgumentAliasOwnerSlots(); this.utf8UncheckedOctets = value.utf8UncheckedOctets; this.utf8MalformedWarning = value.utf8MalformedWarning; this.tainted = value.tainted; this.numericLiteralText = value.numericLiteralText; this.numericContextSeen = value.numericContextSeen; + updateUntrackedReferenceOwner(this.value instanceof RuntimeBase referent + && (this.type & RuntimeScalarType.REFERENCE_BIT) != 0 + ? referent : null); if (packageCodeRefName != null) { GlobalVariable.noteGlobalCodeRefSlotMutation(this); } @@ -2752,6 +3411,9 @@ && isSocketIOHandle(oldIo.ioHandle)) { if ((this.type & RuntimeScalarType.REFERENCE_BIT) != 0 && this.value != null) { oldBase = (RuntimeBase) this.value; } + boolean oldIdentifiedOwner = oldBase != null && this.refCountOwned + && countedOwnerSlot != null && countedOwnerSlot.isActive() + && countedOwnerSlot.referent() == oldBase; // A reference to this scalar keeps its current contents alive through // scalarReferenceContents. Assigning through $$ref transfers that // ownership to the new contents, so the old referent must be released @@ -2762,7 +3424,6 @@ && isSocketIOHandle(oldIo.ioHandle)) { boolean oldOwnedScalarReferenceContents = this.ownsScalarReferenceContents; RuntimeScalar oldScalarReferenceContents = scalarReferenceContentsReferent(this); boolean shouldReleaseUnrootedRescuedGraph = false; - boolean requestedDiscardedAggregateSweep = false; // If this scalar was a weak ref, remove from weak tracking before overwriting. // Weak refs don't count toward refCount, so skip refCount decrement later. @@ -2812,16 +3473,17 @@ && isSocketIOHandle(oldIo.ioHandle)) { // Do the assignment this.type = value.type; this.value = copyScalarPayload(value.type, value.value); + refreshArgumentAliasOwnerSlots(); // An explicitly discarded closure can have an inflated CODE refcount, // so waiting for callDestroy() leaves its captures alive indefinitely. - // Release them only after this slot is gone, the code is inactive, and - // a complete Perl root walk finds no other strong owner (including - // owners stored inside nested containers). + // Release them only when the owner ledger shows no other Perl owner + // besides the scalar slot currently being overwritten. if (value.type == UNDEF && oldBase instanceof RuntimeCode oldCode + && !oldCode.hasCountedCodeSlots && oldCode.capturedScalars != null - && !RuntimeCode.isActiveCode(oldCode) - && !ReachabilityWalker.isReachableFromRoots(oldCode)) { + && !oldCode.hasPendingReturnOwner() + && !oldCode.hasPerlOwnerForCaptureOtherThan(countedOwnerSlot)) { oldCode.releaseCaptures(); } // Hash-element slots remain durable owners when their existing @@ -2839,7 +3501,14 @@ && isSocketIOHandle(oldIo.ioHandle)) { if (this.captureCount > 0 && (this.type & RuntimeScalarType.REFERENCE_BIT) != 0 && this.value instanceof RuntimeBase capturedBase && !WeakRefRegistry.isweak(this)) { + if (this.type == RuntimeScalarType.CODE) { + // Capturing an undef cell precedes assignment in recursive + // closures. The new CODE value needs the capture's own edge, + // independently of the declaring lexical's store owner. + retainClosureCaptureReferent(); + } else { updateClosureOwnerSlot(capturedBase); + } } this.utf8UncheckedOctets = value.utf8UncheckedOctets; this.utf8MalformedWarning = value.utf8MalformedWarning; @@ -2871,7 +3540,7 @@ && hasLiveIo(assignedGlob)) { if (this.globalCodeRefFqn != null && this.value instanceof RuntimeCode code) { code.hadStashRef = true; } - if (packageCodeRefName != null + if ((packageCodeRefName != null || hasCodeSlotBinding()) && (preAssignType != this.type || preAssignValue != this.value)) { GlobalVariable.noteGlobalCodeRefSlotMutation(this); } @@ -2948,17 +3617,46 @@ && hasLiveIo(assignedGlob)) { // and for scalars that didn't own a refCount increment). if (oldBase != null && !thisWasWeak && (this.refCountOwned || oldOwnedByScalarReference)) { - if (oldBase.refCount > 0) { + boolean deferredOldBaseRelease = false; + if (oldBase.refCount > 0 + && oldBase.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS) != 0) { + MortalList.deferDecrementFromArraySlot( + oldBase, this, "RuntimeScalar.setLargeRefCounted overwrite"); + deferredOldBaseRelease = true; + } else if (oldBase.refCount > 0) { oldBase.traceRefCount(-1, "RuntimeScalar.setLargeRefCounted (decrement on overwrite)"); oldBase.releaseOwner(this, "setLargeRefCounted overwrite"); oldBase.releaseActiveOwner(this); } - if (oldBase.refCount > 0 && --oldBase.refCount == 0) { - if (oldBase.localBindingExists) { + if (!deferredOldBaseRelease && oldBase.refCount > 0 && --oldBase.refCount == 0) { + boolean liveScalarReferentBinding = oldBase.localBindingExists + && (!(oldBase instanceof RuntimeScalar scalarReferent) + || !scalarReferent.referencedByScalarReference + || scalarReferent.captureCount > 0 + || MyVarCleanupStack.isRegistered(scalarReferent)); + if (liveScalarReferentBinding) { // Named container (my %hash / my @array): the local variable // 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 (oldIdentifiedOwner && MortalList.canTrustFinalCountedRelease(oldBase)) { + // The final identified edge is gone. Nested calls and + // cached root walks cannot supply a replacement owner. + oldBase.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroyFromCountedOwner(oldBase); + } else if ((oldBase.blessId != 0 + && WeakRefRegistry.hasWeakRefsTo(oldBase) + && oldBase.restoreCountedOwnerSlotRefCount()) + || oldBase.restoreVerifiedCurrentCountedOwnerSlotRefCount()) { + // Verified current scalar cells and aggregate elements + // jointly repair transient undercounts without a root walk. + } else if (oldBase.hasUncountedAggregateOwnerSlotIdentity() + || oldBase.hasUntrackedReferenceOwnerSlotIdentity( + RuntimeScalarType.REFERENCE)) { + // Counted slots have already been repaired above. Any + // remaining aggregate or untracked scalar slot is direct + // evidence of a strong value; defer cleanup until it leaves. + oldBase.deferFinalReleaseToUncountedOwnerSlots(); } else if (oldBase.blessId == 0 && (oldBase instanceof RuntimeArray || oldBase instanceof RuntimeHash) && WeakRefRegistry.hasWeakRefsTo(oldBase) @@ -3034,6 +3732,7 @@ && hasLiveIo(assignedGlob)) { if ((oldBase instanceof RuntimeArray || oldBase instanceof RuntimeHash) && !thisWasWeak && !DestroyDispatch.isRescued(oldBase) + && !oldBase.hasOwnerSlots() && !(oldBase.blessId != 0 && blessedClassHasDestroy(oldBase)) && WeakRefRegistry.hasWeakRefsTo(oldBase) && (RuntimeCode.argsStackDepth() <= 1 @@ -3064,7 +3763,9 @@ && hasLiveIo(assignedGlob)) { MortalList.suppressFlush(flushWasSuppressed); } } - if (undefAssignmentOfDestroyableRef && !DestroyDispatch.isInsideDestroy()) { + if (undefAssignmentOfDestroyableRef + && oldBase.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS) == 0 + && !DestroyDispatch.isInsideDestroy()) { shouldReleaseUnrootedRescuedGraph = true; } @@ -3082,23 +3783,24 @@ && hasLiveIo(assignedGlob)) { && value.type == UNDEF) { MortalList.requestTargetedWeakSweep(oldBase); } - if (undefAssignmentOfDestroyableRef && !ModuleInitGuard.inModuleInit()) { + if (undefAssignmentOfDestroyableRef + && oldBase.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS) == 0 + && !ModuleInitGuard.inModuleInit()) { MortalList.requestTargetedWeakSweep(oldBase); } - // An explicitly discarded aggregate can own a tree of blessed values - // whose selective counts have drifted above zero. Schedule only its - // weakly observed descendants; a complete root walk is unnecessary. - if (value.type == UNDEF - && WeakRefRegistry.weakRefsExist() - && (oldBase instanceof RuntimeHash || oldBase instanceof RuntimeArray)) { - if (MortalList.requestWeakSweepsForDestroyedContainer(oldBase)) { - requestedDiscardedAggregateSweep = true; - } - } - // Update ownership: this scalar now owns a refCount iff we incremented. this.refCountOwned = newOwned; + if (newOwned && this.value instanceof RuntimeBase ownedReferent) { + recordCountedOwner(ownedReferent); + if (untrackedReferenceOwnerSlot != null) { + releaseUntrackedReferenceOwner(untrackedReferenceOwnerSlot.referent()); + } + } else { + updateUntrackedReferenceOwner(this.value instanceof RuntimeBase referent + && (this.type & RuntimeScalarType.REFERENCE_BIT) != 0 + ? referent : null); + } retainScalarReferenceContents(value); // Flush deferred mortal decrements from the current function scope. @@ -3113,10 +3815,6 @@ && hasLiveIo(assignedGlob)) { MortalList.popTemporaryRoot(value); MortalList.popTemporaryRoot(this); } - if (requestedDiscardedAggregateSweep) { - MortalList.flushReleasedWeakSweepsIfSafe(); - } - // An explicit undef can run DESTROY and let the object self-rescue. // Preserve real Perl resurrection when another package/closure root // still reaches that graph. If the only remaining pin is the runtime's @@ -3182,6 +3880,7 @@ public RuntimeScalar set(int value) { } this.type = RuntimeScalarType.INTEGER; this.value = value; + refreshArgumentAliasOwnerSlots(); this.tainted = false; this.numericLiteralText = null; this.numericContextSeen = false; @@ -3199,6 +3898,7 @@ public RuntimeScalar set(long value) { throw new PerlCompilerException("Modification of a read-only value attempted"); } this.initializeWithLong(value); + refreshArgumentAliasOwnerSlots(); this.tainted = false; this.numericLiteralText = null; this.numericContextSeen = false; @@ -3241,6 +3941,7 @@ else if (value.abs().compareTo(BigInteger.valueOf(9007199254740992L)) <= 0) { // this.type = RuntimeScalarType.STRING; this.value = value.toString(); } + refreshArgumentAliasOwnerSlots(); this.tainted = false; this.numericLiteralText = null; this.numericContextSeen = false; @@ -3259,6 +3960,7 @@ public RuntimeScalar set(boolean value) { } this.type = RuntimeScalarType.BOOLEAN; this.value = value; + refreshArgumentAliasOwnerSlots(); this.tainted = false; this.numericLiteralText = null; this.numericContextSeen = false; @@ -3286,6 +3988,7 @@ public RuntimeScalar set(String value) { this.type = RuntimeScalarType.STRING; } this.value = value; + refreshArgumentAliasOwnerSlots(); this.utf8UncheckedOctets = false; this.utf8MalformedWarning = null; this.tainted = false; @@ -3305,6 +4008,7 @@ RuntimeScalar setInternalPackageName(String value) { growingString = null; type = value == null ? UNDEF : RuntimeScalarType.STRING; this.value = value; + refreshArgumentAliasOwnerSlots(); utf8UncheckedOctets = false; utf8MalformedWarning = null; tainted = false; @@ -4149,6 +4853,7 @@ public RuntimeScalar scalarDeref() { RuntimeScalar newScalar = new RuntimeScalar(); this.value = newScalar; this.type = RuntimeScalarType.REFERENCE; + refreshArgumentAliasOwnerSlots(); this.numericLiteralText = null; this.numericContextSeen = false; this.firstClassRegexScalar = false; @@ -4955,7 +5660,6 @@ public RuntimeScalar undefine() { return RuntimeCode.undefineCodeReference(this); } if (type == RuntimeScalarType.CODE && value instanceof RuntimeCode code && globalCodeRefFqn != null) { - boolean releasedCode = false; releaseAllClosureCaptureReferents(code); if (this.refCountOwned && code.refCount > 0) { this.refCountOwned = false; @@ -4963,7 +5667,6 @@ public RuntimeScalar undefine() { if (--code.refCount == 0) { code.refCount = Integer.MIN_VALUE; DestroyDispatch.callDestroy(code); - releasedCode = true; } } // Clear the body but keep a declared CODE slot. Perl preserves the @@ -4988,9 +5691,6 @@ public RuntimeScalar undefine() { this.formatPictureTainted = false; // Invalidate the method resolution cache InheritanceResolver.invalidateCache(); - if (releasedCode && WeakRefRegistry.weakRefsExist() && !ModuleInitGuard.inModuleInit()) { - ReachabilityWalker.sweepWeakRefs(true); - } return this; } @@ -5011,6 +5711,10 @@ public RuntimeScalar undefine() { // For all other types, set to undef this.type = UNDEF; this.value = null; + refreshArgumentAliasOwnerSlots(); + refreshUntrackedReferenceOwner(); + boolean argumentAliasKeepsOldBase = oldBase != null + && oldBase.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS) != 0; this.tainted = false; this.numericLiteralText = null; this.numericContextSeen = false; @@ -5021,7 +5725,23 @@ public RuntimeScalar undefine() { boolean undefOnBlessedWithDestroy = false; boolean undefOnWeakObservedBlessedWrapper = false; if (oldBase != null) { - if (oldBase.refCount == WeakRefRegistry.WEAKLY_TRACKED) { + if (argumentAliasKeepsOldBase) { + if (oldBase.refCount == WeakRefRegistry.WEAKLY_TRACKED) { + oldBase.refCount = 1; + MortalList.deferDecrement(oldBase); + } else if (this.refCountOwned && oldBase.refCount > 0) { + this.refCountOwned = false; + MortalList.deferDecrementFromArraySlot( + oldBase, this, "RuntimeScalar.undefine"); + } + } else if (oldBase.refCount == WeakRefRegistry.WEAKLY_TRACKED + && oldBase.hasPerlStrongOwner()) { + // WEAKLY_TRACKED means the legacy count cannot identify which + // scalar cell is the final owner. The current cell's untracked + // identity was retired above; keep the weak observers while the + // ledger still names another Perl owner. Its final release will + // perform cleanup through onStrongOwnerSlotReleased(). + } else if (oldBase.refCount == WeakRefRegistry.WEAKLY_TRACKED) { // Weakly-tracked object (unblessed, birth-tracked, with weak refs): // clear weak refs on explicit undef. These objects transitioned to // WEAKLY_TRACKED in weaken() because their refCount was unreliable @@ -5085,7 +5805,8 @@ public RuntimeScalar undefine() { // Hash::AutoHash's wraptie constructor has exactly this shape. if (oldBase != null && oldBase.blessId != 0 - && WeakRefRegistry.hasWeakRefsTo(oldBase)) { + && WeakRefRegistry.hasWeakRefsTo(oldBase) + && !argumentAliasKeepsOldBase) { undefOnWeakObservedBlessedWrapper = true; } @@ -5114,15 +5835,6 @@ public RuntimeScalar undefine() { (oldBase != null ? org.perlonjava.runtime.runtimetypes.NameNormalizer.getBlessStr(oldBase.blessId) : "?") + " refCount=" + (oldBase != null ? oldBase.refCount : -1)); } - // The DESTROY path historically needs an immediate global pass so - // callbacks after explicit undef observe the released object as - // gone. A weak-observed wrapper without DESTROY only needs the - // targeted statement-boundary pass: sweeping globally while a - // nested call frame is still assembling objects can clear unrelated - // weak owner links (for example SQL::Translator index -> table). - if (undefOnBlessedWithDestroy) { - ReachabilityWalker.sweepWeakRefs(false); - } } return this; @@ -5176,8 +5888,21 @@ private void closeIOOnDrop() { /** Performs scalar-specific scope cleanup, including owned anonymous IO fds. */ public static void scopeExitCleanup(RuntimeScalar scalar) { - if (scalar == null) return; + scopeExitCleanup(scalar, false); + } + /** Performs final cleanup for a scalar referent that has reached DESTROY. */ + static void scopeExitCleanupDestroyed(RuntimeScalar scalar) { + scopeExitCleanup(scalar, true); + } + + private static void scopeExitCleanup(RuntimeScalar scalar, boolean finalRelease) { + if (scalar == null) return; + if (scalar.hasCodeSlotBinding()) return; + RuntimeBase untrackedValueAtScopeExit = scalar.untrackedReferenceOwnerSlot == null + ? null : scalar.untrackedReferenceOwnerSlot.referent(); + scalar.refreshUntrackedReferenceOwner(); + if (finalRelease) scalar.localBindingExists = false; // ${^LAST_FH} is tied to the lexical handle that performed the last // tell/read. A lexical coercible glob must stop being observable once // that lexical leaves scope, even though the named STDOUT glob remains @@ -5194,9 +5919,10 @@ public static void scopeExitCleanup(RuntimeScalar scalar) { } } - if (scalar.referencedByScalarReference - && scalar.localBindingExists - && scalar.captureCount == 0) { + if (!finalRelease && scalar.referencedByScalarReference + && scalar.captureCount == 0 + && (scalar.localBindingExists || scalar.refCount > 0 + || scalar.hasScalarCellOwner())) { cleanupScalarReferenceBinding(scalar); // A reference to the lexical escaped its declaring scope. The // RuntimeScalar is now the Perl SV container, so its current value @@ -5205,7 +5931,8 @@ public static void scopeExitCleanup(RuntimeScalar scalar) { // scalar self-tie that wrapper owns the remaining counted // reference to this same scalar; returning here would leave the // cycle intact and suppress the handler's DESTROY. - if (scalar.refCount > 0 && scalar.type != RuntimeScalarType.TIED_SCALAR) return; + if ((scalar.refCount > 0 || scalar.hasScalarCellOwner()) + && scalar.type != RuntimeScalarType.TIED_SCALAR) return; } // A captured lexical or an SV with an escaped scalar reference is still @@ -5221,6 +5948,7 @@ public static void scopeExitCleanup(RuntimeScalar scalar) { // - captureCount=0 → capture handling branch not taken if (!scalar.refCountOwned && scalar.captureCount == 0 && !scalar.ioOwner && !scalar.ownsScalarReferenceContents + && scalar.untrackedReferenceOwnerSlot == null && scalar.type != RuntimeScalarType.TIED_SCALAR) { // Special case: CODE refs with unreleased captures that were never // stored via set() (e.g., anonymous subs passed directly as arguments). @@ -5241,44 +5969,21 @@ public static void scopeExitCleanup(RuntimeScalar scalar) { } scalar.releaseOwnedScalarReferenceContents(); + scalar.releaseUnownedClosureCaptures(); + + // RuntimeCode captures carry one SCALAR_CAPTURE owner identity each. + // Retire any remaining compatibility-only capture counts here: they + // have no CODE slot that could keep the pad alive or release the + // count later. Live closures remain protected by their explicit slots. + int explicitCaptureOwners = scalar.ownerSlotCount(PerlOwnerSlot.Kind.SCALAR_CAPTURE); + while (scalar.captureCount > explicitCaptureOwners) { + scalar.releaseClosureCapture(); + } // If this variable is captured by a closure, mark it so releaseCaptures // knows the scope has exited. But still proceed with refCount cleanup below // so that blessed ref refCounts and weak refs are handled properly. if (scalar.captureCount > 0) { - // Self-referential capture cycle detection: if this variable holds - // a CODE ref that captures this same variable, we have a cycle that - // will never resolve on its own. This happens when eval STRING creates - // closures that capture ALL visible lexicals (including the variable - // the closure is assigned to). Break the cycle by decrementing our own - // captureCount and removing ourselves from the CODE's captures array. - // The full release of other captures will happen when the CODE ref's - // refCount reaches 0 (via callDestroy/releaseCaptures). - if (scalar.type == RuntimeScalarType.CODE - && scalar.value instanceof RuntimeCode code - && code.capturedScalars != null) { - boolean selfRef = false; - for (RuntimeScalar s : code.capturedScalars) { - if (s == scalar) { selfRef = true; break; } - } - if (selfRef) { - // Decrement our captureCount (the closure captured us) - scalar.releaseClosureCapture(); - // Remove self from capturedScalars to prevent double-decrement - // when releaseCaptures runs later during CODE ref destruction - RuntimeScalar[] old = code.capturedScalars; - if (old.length == 1) { - code.capturedScalars = null; - } else { - RuntimeScalar[] updated = new RuntimeScalar[old.length - 1]; - int j = 0; - for (RuntimeScalar cap : old) { - if (cap != scalar && j < updated.length) updated[j++] = cap; - } - code.capturedScalars = updated; - } - } - } // Mark that this variable's scope has exited. When releaseCaptures // later decrements captureCount to 0, it will know the scope is gone. scalar.scopeExited = true; @@ -5340,6 +6045,11 @@ public static void scopeExitCleanup(RuntimeScalar scalar) { // when the same referent is on the JVM stack as a return value. MortalList.deferDecrementIfTracked(scalar); + // The lexical may have owned this cell when the initial refresh ran. + // Recheck after scope-exit state changes so its untracked value leaves + // with the final scalar-cell owner instead of waiting for a sweep. + scalar.finalizeUntrackedValueAfterScopeExit(untrackedValueAtScopeExit); + // WEAKLY_TRACKED objects: do NOT clear weak refs on scope exit. // These objects transitioned from untracked (-1) to WEAKLY_TRACKED (-2) in // weaken(), but scope exit of ONE reference doesn't mean no other strong @@ -5415,7 +6125,7 @@ public static void releaseOrTransferSocketIoOwnerOnReturn( RuntimeScalar recipient = returnedSocketScalar(returned, glob); if (recipient == owner) { owner.copiedFromArgumentFrame = null; - owner.containerOwner = null; + owner.reconcileContainerOwners(); return; } if (recipient != null && !recipient.ioOwner) { @@ -5425,7 +6135,7 @@ public static void releaseOrTransferSocketIoOwnerOnReturn( // RuntimeCode.returnList wraps scalar returns in a temporary list. // The returned scalar is a movable value, not a durable element of // that temporary container; let the caller assignment take over. - recipient.containerOwner = null; + recipient.reconcileContainerOwners(); return; } releaseIoOwner(owner); @@ -5475,7 +6185,7 @@ public static void transferIoOwnerToReturnedCopy( source.ioOwner = false; returnedCopy.ioOwner = true; returnedCopy.copiedFromArgumentFrame = null; - returnedCopy.containerOwner = null; + returnedCopy.reconcileContainerOwners(); } private static java.util.List returnedIoAliases( @@ -5583,6 +6293,7 @@ public RuntimeScalar preAutoIncrement() { result = preAutoIncrementWithoutWatcherNotification(); } finally { watcherMutationDepth--; + refreshArgumentAliasOwnerSlots(); } notifyModifiedWatchers(); return result; @@ -5745,6 +6456,7 @@ public RuntimeScalar postAutoIncrement() { result = postAutoIncrementWithoutWatcherNotification(); } finally { watcherMutationDepth--; + refreshArgumentAliasOwnerSlots(); } notifyModifiedWatchers(); return result; @@ -5767,6 +6479,7 @@ public void postAutoIncrementVoid() { postAutoIncrementWithoutWatcherNotification(); } finally { watcherMutationDepth--; + refreshArgumentAliasOwnerSlots(); notifyModifiedWatchers(); } } @@ -5779,6 +6492,7 @@ public RuntimeScalar integerPreAutoIncrement() { result = integerAutoIncrement(false, 1); } finally { watcherMutationDepth--; + refreshArgumentAliasOwnerSlots(); } notifyModifiedWatchers(); return result; @@ -5792,6 +6506,7 @@ public RuntimeScalar integerPostAutoIncrement() { result = integerAutoIncrement(true, 1); } finally { watcherMutationDepth--; + refreshArgumentAliasOwnerSlots(); } notifyModifiedWatchers(); return result; @@ -5805,6 +6520,7 @@ public RuntimeScalar integerPreAutoDecrement() { result = integerAutoIncrement(false, -1); } finally { watcherMutationDepth--; + refreshArgumentAliasOwnerSlots(); } notifyModifiedWatchers(); return result; @@ -5818,6 +6534,7 @@ public RuntimeScalar integerPostAutoDecrement() { result = integerAutoIncrement(true, -1); } finally { watcherMutationDepth--; + refreshArgumentAliasOwnerSlots(); } notifyModifiedWatchers(); return result; @@ -5963,6 +6680,7 @@ private RuntimeScalar postAutoIncrementLarge() { old.refCountOwned = true; old.postfixReferenceOwnershipTransfer = true; this.refCountOwned = false; + transferCountedOwnerTo(old); // Check if object is eligible for overloading int blessId = blessedId(this); if (blessId != 0) { @@ -6027,6 +6745,7 @@ public RuntimeScalar preAutoDecrement() { result = preAutoDecrementWithoutWatcherNotification(); } finally { watcherMutationDepth--; + refreshArgumentAliasOwnerSlots(); } notifyModifiedWatchers(); return result; @@ -6182,6 +6901,7 @@ public RuntimeScalar postAutoDecrement() { result = postAutoDecrementWithoutWatcherNotification(); } finally { watcherMutationDepth--; + refreshArgumentAliasOwnerSlots(); } notifyModifiedWatchers(); return result; @@ -6274,6 +6994,7 @@ private RuntimeScalar postAutoDecrementWithoutWatcherNotification() { old.refCountOwned = true; old.postfixReferenceOwnershipTransfer = true; this.refCountOwned = false; + transferCountedOwnerTo(old); // Check if object is eligible for overloading int blessId = blessedId(this); if (blessId != 0) { @@ -6601,7 +7322,7 @@ private static void cleanupScalarReferenceBinding(RuntimeScalar scalar) { return; } scalar.localBindingExists = false; - if (scalar.refCount == 0 && scalar.blessId != 0) { + if (scalar.refCount == 0 && !scalar.hasScalarCellOwner()) { scalar.refCount = Integer.MIN_VALUE; DestroyDispatch.callDestroy(scalar); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeTiedArrayProxyEntry.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeTiedArrayProxyEntry.java index e0699c53cf..643827bd8d 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeTiedArrayProxyEntry.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeTiedArrayProxyEntry.java @@ -40,6 +40,11 @@ public RuntimeTiedArrayProxyEntry(RuntimeArray parent, RuntimeScalar key, Intege * Vivifies the element by calling FETCH on the tied object. * This ensures lvalue is populated with the current value from the tied array. */ + @Override + RuntimeScalar storageOwnerReference() { + return parent.createAnonymousReference(); + } + @Override void vivify() { if (outOfRangeOriginalIndex != null) { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeTiedHashProxyEntry.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeTiedHashProxyEntry.java index ac8951d697..b8e75db3cf 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeTiedHashProxyEntry.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeTiedHashProxyEntry.java @@ -44,6 +44,11 @@ public RuntimeTiedHashProxyEntry(RuntimeHash parent, RuntimeScalar key) { * Vivifies the element by calling FETCH on the tied object. * This ensures lvalue is populated with the current value from the tied array. */ + @Override + RuntimeScalar storageOwnerReference() { + return parent.createAnonymousReference(); + } + @Override void vivify() { // Always fetch the current value from the tied object diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/SharedElementProxy.java b/src/main/java/org/perlonjava/runtime/runtimetypes/SharedElementProxy.java index b165c34421..d8f9d2872c 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/SharedElementProxy.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/SharedElementProxy.java @@ -12,10 +12,9 @@ final class SharedElementProxy extends RuntimeScalar { super(storage); this.storage = storage; this.value = localView; - // A fetched shared reference is a real, short-lived Perl proxy owner. - // Register that owner so releasing the proxy suppresses DESTROY on its - // runtime-local view while the canonical shared slot still exists. - RuntimeScalar.incrementRefCountForContainerStore(this); + // This wrapper is an expression/lvalue borrow. The receiving Perl slot + // acquires the local view's owner edge; counting the wrapper itself + // leaves an extra owner behind when the fetch result is weakened. } @Override diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/SharedPerlStorage.java b/src/main/java/org/perlonjava/runtime/runtimetypes/SharedPerlStorage.java index adc08d660d..befd692c9a 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/SharedPerlStorage.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/SharedPerlStorage.java @@ -475,6 +475,10 @@ static RuntimeScalar fetchedElement(RuntimeBase owner, RuntimeScalar stored) { PerlRuntime runtime = PerlRuntime.current(); RuntimeBase view = new RuntimeGraphCloner(runtime, runtime).cloneGraph(nested); view.threadSharedFetchedView = true; + // The fetched view can be discarded before any receiving scalar slot + // stores it. Register the local view now so its first release is + // classified as a view cleanup rather than canonical shared DESTROY. + view.registerSharedFetchedView(); return new SharedElementProxy(stored, view); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/StateVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/StateVariable.java index dd50aaa62f..13b637dae5 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/StateVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/StateVariable.java @@ -130,6 +130,11 @@ public static RuntimeScalar retrieveStateScalar(RuntimeScalar codeRef, String va return tagGeneratedLexicalSub(GlobalVariable.getGlobalVariable(beginVar), var); } variable = new RuntimeScalar(); + // The persistent pad is a binding even though this SV is not on + // the per-invocation lexical cleanup stack. A returned \$state + // must keep the cell's value after the CV releases that binding. + variable.refCount = 0; + variable.localBindingExists = true; code.stateVariable.put(beginVar, variable); GlobalVariable.invalidateGlobalCodeRefGraphRoots(); return tagGeneratedLexicalSub(variable, var); @@ -197,7 +202,7 @@ public static void aliasStateArray(RuntimeScalar codeRef, String var, int id, Ru if (!codeRef.getDefinedBoolean()) { GlobalVariable.aliasGlobalArray(beginVar, array); } else { - ((RuntimeCode) codeRef.value).stateArray.put(beginVar, array); + ((RuntimeCode) codeRef.value).bindStateAggregateAlias(beginVar, array); GlobalVariable.invalidateGlobalCodeRefGraphRoots(); } markInitializedStateVariable(codeRef, var, id); @@ -222,7 +227,7 @@ public static void aliasStateHash(RuntimeScalar codeRef, String var, int id, Run if (!codeRef.getDefinedBoolean()) { GlobalVariable.aliasGlobalHash(beginVar, hash); } else { - ((RuntimeCode) codeRef.value).stateHash.put(beginVar, hash); + ((RuntimeCode) codeRef.value).bindStateAggregateAlias(beginVar, hash); GlobalVariable.invalidateGlobalCodeRefGraphRoots(); } markInitializedStateVariable(codeRef, var, id); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/TieArray.java b/src/main/java/org/perlonjava/runtime/runtimetypes/TieArray.java index 90b878caf5..9cc2a8bec7 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/TieArray.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/TieArray.java @@ -62,15 +62,9 @@ public class TieArray extends ArrayList { public TieArray(String tiedPackage, RuntimeArray previousValue, RuntimeScalar self, RuntimeArray parent) { this.tiedPackage = tiedPackage; this.previousValue = previousValue; - this.self = self; + this.self = RuntimeScalar.retainTiedHandlerValue(self); this.parent = parent; - // Increment refCount: the tie wrapper holds a strong reference to the tied object. - if (self != null && (self.type & RuntimeScalarType.REFERENCE_BIT) != 0 - && self.value instanceof RuntimeBase base - && base.refCount >= 0) { - base.refCount++; - base.acquireTransientTraceOwner("tie wrapper", "TieArray"); - } + } /** @@ -302,13 +296,6 @@ public void releaseTiedObject() { if (tiedObjectReleased) return; tiedObjectReleased = true; if (self == null) return; - if ((self.type & RuntimeScalarType.REFERENCE_BIT) != 0 - && self.value instanceof RuntimeBase base) { - base.releaseTransientTraceOwner("tie wrapper", "TieArray"); - if (base.refCount > 0 && --base.refCount == 0) { - base.refCount = Integer.MIN_VALUE; - DestroyDispatch.callDestroy(base); - } - } + RuntimeScalar.releaseTiedHandlerValue(self); } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/TieHandle.java b/src/main/java/org/perlonjava/runtime/runtimetypes/TieHandle.java index cf68a57ad4..568fc3d18d 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/TieHandle.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/TieHandle.java @@ -36,6 +36,7 @@ public class TieHandle extends RuntimeIO { */ private final RuntimeScalar self; private final boolean holdsSelfReference; + private boolean tiedObjectReleased; /** * The package name that this handle is tied to. @@ -61,16 +62,9 @@ public TieHandle(String tiedPackage, RuntimeIO previousValue, RuntimeScalar self public TieHandle(String tiedPackage, RuntimeIO previousValue, RuntimeScalar self, boolean holdSelfReference) { this.tiedPackage = tiedPackage; this.previousValue = previousValue; - this.self = self; + this.self = holdSelfReference ? RuntimeScalar.retainTiedHandlerValue(self) : self; this.holdsSelfReference = holdSelfReference; - // Increment refCount: the tie wrapper holds a strong reference to the tied object. - if (holdSelfReference - && self != null && (self.type & RuntimeScalarType.REFERENCE_BIT) != 0 - && self.value instanceof RuntimeBase base - && base.refCount >= 0) { - base.refCount++; - base.acquireTransientTraceOwner("tie wrapper", "TieHandle"); - } + } /** @@ -355,16 +349,8 @@ public String toString() { * Decrements refCount and triggers DESTROY if it reaches 0. */ public void releaseTiedObject() { - if (!holdsSelfReference) { - return; - } - if ((self.type & RuntimeScalarType.REFERENCE_BIT) != 0 - && self.value instanceof RuntimeBase base) { - base.releaseTransientTraceOwner("tie wrapper", "TieHandle"); - if (base.refCount > 0 && --base.refCount == 0) { - base.refCount = Integer.MIN_VALUE; - DestroyDispatch.callDestroy(base); - } - } + if (!holdsSelfReference || tiedObjectReleased) return; + tiedObjectReleased = true; + RuntimeScalar.releaseTiedHandlerValue(self); } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/TieHash.java b/src/main/java/org/perlonjava/runtime/runtimetypes/TieHash.java index 77cdbfb644..c25fe556fe 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/TieHash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/TieHash.java @@ -54,14 +54,7 @@ public class TieHash extends HashMap { public TieHash(String tiedPackage, RuntimeHash previousValue, RuntimeScalar self) { this.tiedPackage = tiedPackage; this.previousValue = previousValue; - this.self = self; - // Increment refCount: the tie wrapper holds a strong reference to the tied object. - if (self != null && (self.type & RuntimeScalarType.REFERENCE_BIT) != 0 - && self.value instanceof RuntimeBase base - && base.refCount >= 0) { - base.refCount++; - base.acquireTransientTraceOwner("tie wrapper", "TieHash"); - } + this.self = RuntimeScalar.retainTiedHandlerValue(self); if (self != null && (self.type & RuntimeScalarType.REFERENCE_BIT) != 0 && self.value instanceof RuntimeBase base) { base.markPossiblyStoredInTiedHandler(); @@ -228,13 +221,6 @@ public void releaseTiedObject() { if (tiedObjectReleased) return; tiedObjectReleased = true; if (self == null) return; - if ((self.type & RuntimeScalarType.REFERENCE_BIT) != 0 - && self.value instanceof RuntimeBase base) { - base.releaseTransientTraceOwner("tie wrapper", "TieHash"); - if (base.refCount > 0 && --base.refCount == 0) { - base.refCount = Integer.MIN_VALUE; - DestroyDispatch.callDestroy(base); - } - } + RuntimeScalar.releaseTiedHandlerValue(self); } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/TiedVariableBase.java b/src/main/java/org/perlonjava/runtime/runtimetypes/TiedVariableBase.java index ef0b83f7bc..fec406a7fd 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/TiedVariableBase.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/TiedVariableBase.java @@ -35,20 +35,9 @@ public abstract class TiedVariableBase extends RuntimeBaseProxy { */ public TiedVariableBase(RuntimeScalar tiedObject, String tiedPackage) { super(); - this.self = tiedObject; + this.self = RuntimeScalar.retainTiedHandlerValue(tiedObject); this.tiedPackage = tiedPackage; - // The tie wrapper holds a strong reference to the tied object. - // Increment refCount so that untie's decrement can trigger DESTROY - // when no other references remain (matching Perl 5 behavior). - // Note: tiedObject is null for proxy entries (RuntimeTiedHashProxyEntry, - // RuntimeTiedArrayProxyEntry) which delegate to the parent container. - if (tiedObject != null - && (tiedObject.type & RuntimeScalarType.REFERENCE_BIT) != 0 - && tiedObject.value instanceof RuntimeBase base - && base.refCount >= 0) { - base.refCount++; - base.acquireTransientTraceOwner("tie wrapper", "TiedVariableBase"); - } + } /** @@ -212,6 +201,11 @@ public RuntimeScalar addToScalar(RuntimeScalar v) { return super.addToScalar(v); } + /** Storage kept alive by an escaping magic lvalue; this is a borrowed reference. */ + RuntimeScalar storageOwnerReference() { + return self; + } + public RuntimeScalar getSelf() { return self; } @@ -231,13 +225,6 @@ public void releaseTiedObject() { // Array/hash element proxy entries delegate to their parent tied // container and intentionally have no handler of their own. if (self == null) return; - if ((self.type & RuntimeScalarType.REFERENCE_BIT) != 0 - && self.value instanceof RuntimeBase base) { - base.releaseTransientTraceOwner("tie wrapper", "TiedVariableBase"); - if (base.refCount > 0 && --base.refCount == 0) { - base.refCount = Integer.MIN_VALUE; - DestroyDispatch.callDestroy(base); - } - } + RuntimeScalar.releaseTiedHandlerValue(self); } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/WeakRefRegistry.java b/src/main/java/org/perlonjava/runtime/runtimetypes/WeakRefRegistry.java index 5811970656..0b9379be97 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/WeakRefRegistry.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/WeakRefRegistry.java @@ -111,15 +111,22 @@ private static void weaken( } else { ref.type = RuntimeScalarType.UNDEF; ref.value = null; + ref.refreshArgumentAliasOwnerSlots(); return; } } state.weakScalars.add(ref); + ref.refreshArgumentAliasOwnerSlots(); state.referentToWeakRefs .computeIfAbsent(base, k -> Collections.newSetFromMap(new IdentityHashMap<>())) .add(ref); base.activateOwnerTracking(); + ref.releaseUntrackedReferenceOwner(base); + // Releasing the final strong slot can synchronously run DESTROY and + // clear this newly registered weak observer, as Perl does when the + // weakened variable was the last owner. + if (!state.weakScalars.contains(ref) || ref.value != base) return; ref.releaseClosureCaptureReferentsForWeaken(base); if (System.getenv("PJ_WEAKCLEAR_TRACE") != null) { System.err.println("[WEAKEN] ref=" + System.identityHashCode(ref) @@ -147,11 +154,10 @@ private static void weaken( boolean weakenedLiveLexical = MyVarCleanupStack.isLive(ref); boolean weakenedLiveCodeRef = base instanceof RuntimeCode && weakenedLiveLexical; - boolean weakenedLiveBlessedRef = base.blessId != 0 - && !(base instanceof RuntimeCode) - && weakenedLiveLexical; - if (base.refCount > 0 && ref.refCountOwned) { + PerlOwnerSlot releasedSlot = ref.countedOwnerSlot(); + boolean identifiedOwner = releasedSlot != null && releasedSlot.isActive() + && releasedSlot.referent() == base; // Tracked object with a properly-counted reference: // decrement strong count (weak ref doesn't count). // Only decrement if refCountOwned=true, meaning the hash element @@ -168,7 +174,9 @@ private static void weaken( base.releaseOwner(ref, "weaken"); base.releaseActiveOwner(ref); if (--base.refCount == 0) { - if (base.hasSemanticCaptureOwner()) { + if (base instanceof RuntimeCode code && code.hasActiveCodeOwner()) { + code.deferFinalReleaseToActiveCode(); + } else if (base.hasSemanticCaptureOwner()) { // A captured pad is an authoritative Perl owner even when // the weak probe being weakened held the last ordinary // counted slot. Keep the referent alive until the shared @@ -180,12 +188,28 @@ private static void weaken( // 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 (!(base instanceof RuntimeScalar scalar + } else if (base.restoreCountedOwnerSlotRefCount()) { + // A surviving counted identity is direct evidence of a Perl + // owner. Repair a transient legacy undercount before asking + // the root walker to rediscover the same scalar/container edge. + } else if (!(identifiedOwner && snapshotRoots.isEmpty() + && MortalList.canTrustFinalCountedRelease(base)) + && !(base instanceof RuntimeScalar scalar + && scalar.ephemeralScalarReferenceReferent) + && hasWeakRefsTo(base) + && (base.hasUntrackedReferenceOwnerSlotIdentity() + || base.hasDirectOwnerSlotProofForReleasedWeakReferent())) { + // Refresh each untracked scalar owner against its current + // durable storage before accepting it. A stale slot must + // not turn a final release into a WEAKLY_TRACKED referent + // that waits for a later sweep to clear its weak observers. + base.refCount = WEAKLY_TRACKED; + } else if (!(identifiedOwner && snapshotRoots.isEmpty() + && MortalList.canTrustFinalCountedRelease(base)) + && !(base instanceof RuntimeScalar scalar && scalar.ephemeralScalarReferenceReferent) && hasWeakRefsTo(base) - && (ReachabilityWalker.isReachableFromRoots(base) - || ReachabilityWalker.isReachableFromLiveScalarRegistry(base) - || ReachabilityWalker.isReachableFromLiveCodeCaptures(base) + && (ReachabilityWalker.isReachableFromLiveCodeCaptures(base) || ReachabilityWalker.isReachableFromStrongRoots(base, snapshotRoots) || RuntimeCode.isInstalledPadConstant(base))) { // A temporary probe can be weakened without owning the last @@ -199,7 +223,12 @@ && hasWeakRefsTo(base) // No local binding: refCount==0 means truly no strong refs. // Trigger DESTROY + clear weak refs. base.refCount = Integer.MIN_VALUE; - DestroyDispatch.callDestroy(base); + if (identifiedOwner && snapshotRoots.isEmpty() + && MortalList.canTrustFinalCountedRelease(base)) { + DestroyDispatch.callDestroyFromCountedOwner(base); + } else { + DestroyDispatch.callDestroy(base); + } } } // Note: we do NOT transition unblessed tracked objects to WEAKLY_TRACKED @@ -214,9 +243,7 @@ && hasWeakRefsTo(base) } else if (base.refCount == -1 && base instanceof RuntimeScalarReadOnly && RuntimeCode.isInstalledPadConstant(base)) { - // The defining subroutine's pad is the strong owner. Its ownership - // is not represented in selective refCount, and glob replacement - // clears the weak ref explicitly via clearPadConstantWeakRefs(). + // A live CV optree owns this constant through an explicit owner slot. } else if (base.refCount == -1 && !(base instanceof RuntimeCode)) { // Untracked non-CODE object: transition to WEAKLY_TRACKED so that // undefine() and scopeExitCleanup() can clear weak refs @@ -237,32 +264,32 @@ && hasWeakRefsTo(base) // (which use weaken() for back-references) and cascades to break // Moo's accessor inlining (51 test failures). See §15. ref.refCountOwned = false; - if (base.blessId == 0 + boolean legacyPackageNestedAggregate = base.blessId == 0 && (base instanceof RuntimeArray || base instanceof RuntimeHash) - && ReachabilityWalker.isReachableFromRoots(base)) { - // An untracked aggregate may already be owned by a live tree - // before its first weak parent edge is installed. Keep it - // through this transition; a later full sweep can clear the - // observer after that Perl root actually leaves scope. + && base.storedInPackageGlobal + && base.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE) != 0; + if (legacyPackageNestedAggregate + && ReachabilityWalker.hasLiveUntrackedOwnerSlot(base)) { + // Keep the existing compatibility count at this unmigrated + // package-root boundary, but give that temporary pin an exact + // identity. The scalar owner slots remain the Perl ownership + // evidence and release this bridge when the last one leaves. base.refCount = 1; + base.packageRootRescuedUntrackedAggregate = true; + if (base.packageRootRescueOwnerSlot == null + || !base.packageRootRescueOwnerSlot.isActive()) { + base.packageRootRescueOwnerSlot = new PerlOwnerSlot( + PerlOwnerSlot.Kind.WEAKLY_TRACKED_COMPATIBILITY_PIN); + } + base.packageRootRescueOwnerSlot.acquireWeaklyTrackedCompatibilityPin(base); } else { + // Keep the compatibility sentinel while an untracked value is + // between expression evaluation and installation in a durable + // slot. Owner release hooks finish weak cleanup; do not infer + // Perl ownership from a general root-graph walk here. base.refCount = WEAKLY_TRACKED; - MortalList.requestImmediateWeakSweep(); } } - // A live lexical can be the final Perl-visible strong owner even when - // selective refCount remains inflated by earlier argument/temporary - // copies. Request one root-based sweep at the next safe statement - // boundary so weaken($lexical) has Perl's immediate observable - // semantics. Keep this limited to blessed non-CODE referents: DBIC - // schema/parser flows need it, while CODE refs and container-internal - // weak backrefs use their targeted cleanup paths below. - if (weakenedLiveBlessedRef - && base.refCount > 0 - && base.reachableOwnerCount() == 0 - && !ModuleInitGuard.inModuleInit()) { - MortalList.requestImmediateWeakSweep(); - } boolean shouldCheckLiveCodeRef = weakenedLiveCodeRef && codeRefHasCountedOwners(base) && !ModuleInitGuard.inModuleInit(); @@ -282,8 +309,11 @@ && codeRefHasCountedOwners(base) // their weak registry entries immediately. if (hasWeakRefsTo(code) && code.stashRefCount <= 0 - && !isInstalledGlobalCodeRef(code) - && (code.hadStashRef || code.reachableOwnerCount() == 0)) { + && !isInstalledGlobalCodeRef(code)) { + // clearWeakRefsTo() makes the final decision from stash state, + // active frames, and explicit owner-slot identities. A scalar + // root walk here duplicates that proof and misses the purpose + // of the owner ledger. clearWeakRefsTo(code); } } @@ -323,8 +353,10 @@ public static void unweaken(RuntimeScalar ref) { base.recordOwner(ref, "WeakRefRegistry.unweaken"); base.recordActiveOwner(ref); } + RuntimeScalar.incrementRefCountForContainerStore(ref); // Note: if MIN_VALUE, object already destroyed — unweaken is a no-op } + ref.refreshArgumentAliasOwnerSlots(); } /** @@ -359,11 +391,154 @@ public static boolean hasWeakRefsTo(RuntimeBase referent) { return weakRefs != null && !weakRefs.isEmpty(); } + /** Finish tracked cleanup when its final owner slot leaves. */ + static void onStrongOwnerSlotReleased(RuntimeBase referent) { + if (referent instanceof RuntimeCode code) { + code.releasePadConstantsForUntrackedOwnerSlot(); + } + boolean zeroCountOwnerSlotsExhausted = referent != null + && referent.refCount == 0 + && !(referent instanceof RuntimeCode) + && !GlobalVariable.isPackageRootedAggregate(referent) + && MortalList.canTrustFinalCountedRelease(referent); + if (referent != null + && ((referent.deferredFinalReleaseForUncountedOwners + && referent.refCount == WEAKLY_TRACKED) + || zeroCountOwnerSlotsExhausted) + && !referent.hasOwnerSlots() + && PerlRuntime.currentOrNull() != null + && !referent.localBindingExists + && referent.captureCount == 0 + && !referent.hasPendingReturnOwner() + && referent.countedOwnerSlotCount() == 0 + && !(referent instanceof RuntimeScalarReadOnly + && RuntimeCode.isInstalledPadConstant(referent))) { + referent.deferredFinalReleaseForUncountedOwners = false; + referent.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroy(referent); + return; + } + PerlOwnerSlot packagePin = referent == null + ? null : referent.packageRootRescueOwnerSlot; + if (referent != null + && referent.packageRootRescuedUntrackedAggregate + && packagePin != null + && packagePin.isActive() + && referent.refCount > 0 + && referent.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE) == 0 + && PerlRuntime.currentOrNull() != null + && !(referent instanceof RuntimeScalarReadOnly + && RuntimeCode.isInstalledPadConstant(referent))) { + boolean hasWeakObservers = hasWeakRefsTo(referent); + boolean hasOtherPerlOwner = referent.refCount > 1 + || referent.localBindingExists + || referent.captureCount > 0 + || referent.hasPendingReturnOwner() + || referent.countedOwnerSlotCount() != 0 + || referent.hasUncountedPerlOwnerSlotIdentity(); + referent.packageRootRescuedUntrackedAggregate = false; + referent.packageRootRescueOwnerSlot = null; + boolean finalWeakRelease = !hasOtherPerlOwner && hasWeakObservers; + if (finalWeakRelease) { + referent.refCount = Integer.MIN_VALUE; + } else if (hasOtherPerlOwner && referent.refCount == 1 && hasWeakObservers) { + referent.refCount = WEAKLY_TRACKED; + } else { + referent.refCount = referent.refCount == 1 + ? -1 : referent.refCount - 1; + } + packagePin.release(); + if (finalWeakRelease) DestroyDispatch.callDestroy(referent); + return; + } + if (referent == null + || referent.refCount != WEAKLY_TRACKED + || referent.hasOwnerSlots() + || PerlRuntime.currentOrNull() == null + || !hasWeakRefsTo(referent) + || referent.localBindingExists + || referent.captureCount > 0 + || referent.hasPendingReturnOwner() + || referent.countedOwnerSlotCount() != 0 + || (referent instanceof RuntimeScalarReadOnly + && RuntimeCode.isInstalledPadConstant(referent))) return; + + if (ReachabilityWalker.correctiveSweepsEnabled()) { + MortalList.requestTargetedWeakSweep(referent); + return; + } + + if (referent.blessId != 0 + || referent instanceof RuntimeArray + || referent instanceof RuntimeHash) { + referent.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroy(referent); + } else { + clearWeakRefsTo(referent); + } + } + + /** Finalize an untracked value after its scalar owner has left scope. */ + static void finalizeUntrackedAfterOwnerScopeExit(RuntimeBase referent) { + if (referent == null || referent.refCount != -1 + || PerlRuntime.currentOrNull() == null + || referent.hasOwnerSlots() || referent.localBindingExists + || referent.captureCount > 0 || referent.hasPendingReturnOwner() + || referent.countedOwnerSlotCount() != 0 + || MortalList.temporaryRootDirectlyReferences(referent) + || (referent instanceof RuntimeCode code && RuntimeCode.isActiveCode(code))) { + return; + } + referent.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroy(referent); + } + + /** Finish an aggregate's cleanup after its active lexical pad owner left. */ + static void onActiveLexicalOwnerReleased(RuntimeBase referent) { + if (referent == null || PerlRuntime.currentOrNull() == null + || referent.hasOwnerSlots() || referent.captureCount > 0 + || referent.hasPendingReturnOwner() + || referent.countedOwnerSlotCount() != 0 + || referent.localBindingExists || referent.refCount > 0 + || referent.refCount == Integer.MIN_VALUE) return; + + boolean untrackedAggregate = referent.blessId == 0 + && (referent instanceof RuntimeArray || referent instanceof RuntimeHash); + if (untrackedAggregate && referent.refCount == 1) { + referent.refCount = WEAKLY_TRACKED; + } + + // Keep the compatibility path's targeted scheduling when explicitly + // enabled. The default-off path below finalizes directly from slot + // identity and does not need a later reachability pass. + if (ReachabilityWalker.correctiveSweepsEnabled()) { + if (referent.refCount == WEAKLY_TRACKED && hasWeakRefsTo(referent)) { + MortalList.requestTargetedWeakSweep(referent); + } + return; + } + + if (referent.blessId != 0) { + referent.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroy(referent); + return; + } + + if ((referent.refCount == WEAKLY_TRACKED || referent.refCount == 0) + && hasWeakRefsTo(referent)) { + clearWeakRefsTo(referent); + } + } + /** * Clear all weak references to a referent. Called when refCount reaches 0, * before DESTROY. Sets all weak scalars pointing to this referent to undef. */ public static void clearWeakRefsTo(RuntimeBase referent) { + clearWeakRefsTo(referent, false); + } + + static void clearWeakRefsTo(RuntimeBase referent, boolean finalCountedOwner) { // A captured pad is a semantic strong owner even when the selective // refcount has reached a weak-tracking sentinel. All destruction and // sweeping paths funnel through this method, so preserve that edge at @@ -375,7 +550,8 @@ public static void clearWeakRefsTo(RuntimeBase referent) { // Sub::Quote/Sub::Defer store weak backrefs to anonymous deferred subs; // leaving those weak scalars defined keeps the whole quoted/deferred // metadata graph Java-reachable and leaks until global destruction. - if (referent instanceof RuntimeCode code && shouldKeepCodeWeakRefs(code)) return; + if (!finalCountedOwner && referent instanceof RuntimeCode code + && shouldKeepCodeWeakRefs(code)) return; LifecycleRuntimeState state = state(); Set weakRefs = state.referentToWeakRefs.remove(referent); if (weakRefs == null) return; @@ -402,18 +578,56 @@ private static boolean isInstalledGlobalCodeRef(RuntimeCode code) { } private static boolean shouldKeepCodeWeakRefs(RuntimeCode code) { - if (code.stashRefCount > 0 || isInstalledGlobalCodeRef(code)) return true; + boolean installed = isInstalledGlobalCodeRef(code); + boolean trace = System.getenv("PJ_WEAKCLEAR_TRACE") != null + && code.hadStashRef && code.stashRefCount <= 0 && !installed; + if (trace) { + System.err.println("[WEAKCODE-CHECK] code=" + System.identityHashCode(code) + + " refCount=" + code.refCount + " stashRefCount=" + code.stashRefCount + + " installed=" + installed + " slots=" + code.ownerSlotSnapshot().size()); + } + if (code.stashRefCount > 0 || installed) return true; // The active frame is already a definitive strong owner. Check it // before scanning the live scalar registries, which can be large and // sits on the callback dispatch path when a quiet sweep runs. if (RuntimeCode.isActiveCode(code)) return true; - LifecycleRuntimeState lifecycleState = state(); - Set sweepLiveReferents = lifecycleState.weakSweepLiveReferents; - if (sweepLiveReferents != null && sweepLiveReferents.contains(code)) return true; - ReachabilityWalker.RootReachabilitySnapshot fullRootSnapshot = - lifecycleState.fullRootSnapshot; - if (fullRootSnapshot != null && fullRootSnapshot.isReachable(code)) return true; - if (ReachabilityWalker.hasLiveStrongScalarReferent(code)) return true; + // Stale frame-temporary scalar cells can leave counted identities on + // a CODE after their Perl scope exits. Retire those cells before using + // the remaining owner ledger as direct liveness evidence. + code.releaseTemporaryCodeScalarOwners(); + if (hasWeakPreservingCodeOwnerSlot(code)) { + if (trace) { + for (PerlOwnerSlot slot : code.ownerSlotSnapshot()) { + RuntimeScalar owner = slot.countedScalarOwner(); + System.err.println("[WEAKCODE-SLOT] kind=" + slot.kind() + + " id=" + slot.identity() + " native=" + slot.isNativeOwner() + + " owner=" + (owner == null ? "null" + : System.identityHashCode(owner)) + + " ownerType=" + (owner == null ? "-" : owner.type) + + " valueIsCode=" + (owner != null && owner.value == code) + + " refCountOwned=" + (owner != null && owner.refCountOwned) + + " registered=" + (owner != null && MyVarCleanupStack.isRegistered(owner)) + + " scopeExited=" + (owner != null && owner.scopeExited) + + " local=" + (owner != null && owner.localBindingExists) + + " package=" + (owner != null && owner.isPackageGlobalRoot) + + " container=" + (owner != null && owner.containerOwner != null)); + } + } + return true; + } + // A Perl-visible owner must have an active slot identity. Sweep-local + // reachability snapshots and scalar registry scans are not ownership. + return false; + } + + private static boolean hasWeakPreservingCodeOwnerSlot(RuntimeCode code) { + for (PerlOwnerSlot slot : code.ownerSlotSnapshot()) { + // CODE_SLOT identities also represent compatibility pins retained + // for compiled call sites after a glob is deleted. The visible + // stash checks above distinguish live Perl CODE owners from those + // Java-facing pins. + if (slot.isActive() && slot.kind() != PerlOwnerSlot.Kind.CODE_SLOT) return true; + } return false; } @@ -441,41 +655,4 @@ public static java.util.List snapshotWeakRefReferents() { return new java.util.ArrayList<>(state().referentToWeakRefs.keySet()); } - public static void clearAllBlessedWeakRefs() { - clearBlessedWeakRefsExcept(java.util.Collections.emptySet()); - } - - /** - * Clear weak references whose referents cannot be observed by END code. - * This is the pre-END counterpart to {@link #clearAllBlessedWeakRefs()}: - * genuine END captures remain intact, while stale selective-refcount - * owners do not make leak tracers report objects that Perl has released. - */ - public static void clearBlessedWeakRefsExcept(Set retained) { - // Snapshot the keys to avoid ConcurrentModificationException, - // since clearWeakRefsTo modifies referentToWeakRefs. - java.util.List referents = - new java.util.ArrayList<>(state().referentToWeakRefs.keySet()); - for (RuntimeBase referent : referents) { - if (retained.contains(referent)) continue; - if (referent instanceof RuntimeCode) continue; - // Phase H3: skip unblessed containers (ARRAY/HASH) at pre-END - // time. Sub::Defer's $deferred_info and Sub::Quote's - // $quoted_info are reachable only via closure captures not - // traversed by `clearAllBlessedWeakRefs`. Clearing them - // breaks END-block leak-tracer dispatch loops that call - // Moo accessors to stringify weak-registry slots. - if (referent.blessId == 0 && !(referent instanceof RuntimeScalar)) continue; - // Phase I: skip clearing weak refs to scalars that hold CODE - // refs or are UNDEF (Sub::Quote/Sub::Defer slot scalars). - if (referent instanceof RuntimeScalar s) { - if (s.type == RuntimeScalarType.UNDEF) continue; - if ((s.type & RuntimeScalarType.REFERENCE_BIT) != 0 - && s.value instanceof RuntimeCode) { - continue; - } - } - clearWeakRefsTo(referent); - } - } } diff --git a/src/test/java/org/perlonjava/runtime/regex/RuntimeRegexCallbackOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/regex/RuntimeRegexCallbackOwnerSlotTest.java new file mode 100644 index 0000000000..c218e00158 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/regex/RuntimeRegexCallbackOwnerSlotTest.java @@ -0,0 +1,53 @@ +package org.perlonjava.runtime.regex; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.perlonjava.runtime.runtimetypes.MortalList; +import org.perlonjava.runtime.runtimetypes.PerlOwnerSlot; +import org.perlonjava.runtime.runtimetypes.PerlRuntime; +import org.perlonjava.runtime.runtimetypes.RuntimeArray; +import org.perlonjava.runtime.runtimetypes.RuntimeBase; +import org.perlonjava.runtime.runtimetypes.RuntimeCode; +import org.perlonjava.runtime.runtimetypes.RuntimeScalar; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class RuntimeRegexCallbackOwnerSlotTest { + @Test + void finalCallbackOwnerReleasesCapturesDespiteStaleCodeCount() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray capturedPad = new RuntimeArray(); + RuntimeCode code = new RuntimeCode(null, List.of()); + code.refCount = 0; + PerlOwnerSlot padOwner = capturedPad.retainClosureCaptureOwner(code); + code.capturedAggregates = new RuntimeBase[] {capturedPad}; + code.capturedAggregateOwnerSlots = new PerlOwnerSlot[] {padOwner}; + code.captureFieldsRecorded = true; + + RuntimeRegexCallback callback = (RuntimeRegexCallback) + RuntimeRegexCallback.wrap(new RuntimeScalar(code), "BLOCK").value; + callback.retainOwner(); + assertTrue(code.hasPerlOwnerForCaptureOtherThan(null)); + + code.refCount = 8; // compatibility count outlives the callback owner + MortalList.scopeExitCleanupArray(capturedPad); + assertTrue(padOwner.isActive(), "the callback owner keeps its captured pad alive"); + + callback.releaseOwner(); + + assertTrue(code.refCount > 0, "the regression requires a stale positive count"); + assertFalse(code.hasPerlOwnerForCaptureOtherThan(null)); + assertNull(code.capturedAggregates); + assertNull(code.capturedAggregateOwnerSlots); + assertFalse(padOwner.isActive()); + assertEquals(0, capturedPad.captureCount); + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/ActiveArgumentOwnerLedgerFastPathTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/ActiveArgumentOwnerLedgerFastPathTest.java new file mode 100644 index 0000000000..8333c21dc2 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/ActiveArgumentOwnerLedgerFastPathTest.java @@ -0,0 +1,56 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class ActiveArgumentOwnerLedgerFastPathTest { + @Test + void countedCallerCellAvoidsReferentScanAndStillReleasesAtCallEnd() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash target = new RuntimeHash(); + target.blessId = NameNormalizer.getBlessId("Issue1649::ActiveArgumentLedger"); + RuntimeScalar released = new RuntimeScalar(); + released.set(target.createAnonymousReference()); + RuntimeScalar callerCell = new RuntimeScalar(); + callerCell.set(target.createAnonymousReference()); + RuntimeScalar weak = target.createAnonymousReference(); + WeakRefRegistry.weaken(weak); + RuntimeCode.CallArguments arguments = RuntimeCode.newCallArguments(1); + arguments.add(callerCell); + + try { + assertEquals(0, target.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE), + "a persistent caller cell remains an aliased borrow"); + target.refCount = 1; // Model a transient legacy-count underflow. + MortalList.deferDecrementIfTracked(released); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + MortalList.flushAboveMark(); + + assertEquals(1, target.refCount); + assertEquals(1, target.countedOwnerSlotCount()); + assertTrue(runtime.lifecycleState.deferredArgumentOwnerReleases.isEmpty(), + "the owner ledger avoids scanning the active argument frame"); + assertTrue(WeakRefRegistry.isweak(weak)); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + arguments.finish(); + callerCell.set(new RuntimeScalar()); + assertEquals(RuntimeScalarType.UNDEF, weak.type, + "clearing the caller's final slot clears the weak observer"); + assertEquals(0, target.countedOwnerSlotCount()); + } finally { + arguments.finish(); + if (WeakRefRegistry.isweak(weak)) WeakRefRegistry.unweaken(weak); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/ActiveCodeOwnerTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/ActiveCodeOwnerTest.java new file mode 100644 index 0000000000..87f4a685b6 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/ActiveCodeOwnerTest.java @@ -0,0 +1,147 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class ActiveCodeOwnerTest { + @Test + void borrowedCodeIsNotFinalizedWithoutACountedOwnerRelease() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar pad = new RuntimeScalar(42); + RuntimeCode code = new RuntimeCode((args, context) -> new RuntimeList(), null); + code.refCount = 0; + code.capturedScalars = new RuntimeScalar[]{pad}; + code.capturedScalarOwnerSlots = new PerlOwnerSlot[]{pad.retainClosureCaptureSlot()}; + RuntimeCode.pushActiveCode(code); + RuntimeCode.popActiveCode(code); + assertNotNull(code.capturedScalars); + assertEquals(1, pad.captureCount); + assertEquals(0, code.nativeOwnerSlotCount()); + code.releaseCaptures(); + assertEquals(0, pad.captureCount); + } + } + + @Test + void recursiveFramesHaveDistinctNativeOwnersAndReleaseAtTheLastReturn() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeCode code = new RuntimeCode((args, context) -> new RuntimeList(), null); + code.refCount = 0; + RuntimeScalar owner = new RuntimeScalar(); + owner.set(new RuntimeScalar(code)); + RuntimeScalar weak = new RuntimeScalar(); + weak.set(owner); + WeakRefRegistry.weaken(weak); + RuntimeCode.pushActiveCode(code); + RuntimeCode.pushActiveCode(code); + var identities = new HashSet(); + for (PerlOwnerSlot slot : code.ownerSlotSnapshot()) { + if (slot.kind() == PerlOwnerSlot.Kind.ACTIVE_CODE) { + assertTrue(slot.isNativeOwner()); + identities.add(slot.identity()); + } + } + assertEquals(2, identities.size()); + assertEquals(2, code.nativeOwnerSlotCount()); + assertEquals(0, runtime.positiveOwnerSlotReferentCount(code)); + owner.set(new RuntimeScalar()); + assertEquals(0, code.refCount); + assertEquals(RuntimeScalarType.CODE, weak.type); + RuntimeCode.popActiveCode(code); + assertEquals(1, code.nativeOwnerSlotCount()); + assertEquals(RuntimeScalarType.CODE, weak.type); + try (MortalList.ReachabilityQueryMeasurement measurement = MortalList.measureReachabilityQueries()) { + RuntimeCode.popActiveCode(code); + assertEquals(0, code.nativeOwnerSlotCount()); + assertEquals(RuntimeScalarType.UNDEF, weak.type); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, code.perlLifecycleState()); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + assertEquals(0, measurement.stats().scalarReachabilityQueries); + } + } + } + + @Test + void activeFrameProtectsCapturesUntilTheCallerDrainsTheirRetiredOwners() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash guard = new RuntimeHash(); + RuntimeScalar pad = new RuntimeScalar(); + pad.set(guard.createAnonymousReference()); + RuntimeScalar weak = new RuntimeScalar(); + weak.set(pad); + WeakRefRegistry.weaken(weak); + RuntimeCode code = new RuntimeCode((args, context) -> new RuntimeList(), null); + code.refCount = 0; + code.capturedScalars = new RuntimeScalar[]{pad}; + code.capturedScalarOwnerSlots = new PerlOwnerSlot[]{pad.retainClosureCaptureSlot()}; + RuntimeScalar.scopeExitCleanup(pad); + RuntimeCode.pushActiveCode(code); + code.releaseCaptures(); + assertNotNull(code.capturedScalars); + assertEquals(1, pad.captureCount); + assertEquals(RuntimeScalarType.HASHREFERENCE, weak.type); + RuntimeCode.popActiveCode(code); + MortalList.flushAboveMark(); + assertNull(code.capturedScalars); + assertEquals(0, pad.captureCount); + assertEquals(RuntimeScalarType.UNDEF, weak.type); + } + } + + @Test + void removingAnOuterFrameRetiresOneIdentityAndPreservesOtherRecursiveFrames() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeCode code = new RuntimeCode((args, context) -> new RuntimeList(), null); + RuntimeCode other = new RuntimeCode((args, context) -> new RuntimeList(), null); + RuntimeCode.pushActiveCode(code); + RuntimeCode.pushActiveCode(code); + RuntimeCode.pushActiveCode(other); + RuntimeCode.popActiveCode(code); + assertEquals(1, code.ownerSlotCount(PerlOwnerSlot.Kind.ACTIVE_CODE)); + assertEquals(1, other.ownerSlotCount(PerlOwnerSlot.Kind.ACTIVE_CODE)); + RuntimeCode.popActiveCode(other); + RuntimeCode.popActiveCode(code); + assertFalse(code.hasActiveCodeOwner()); + assertFalse(other.hasActiveCodeOwner()); + assertTrue(runtime.executionState().activeCodeStack.isEmpty()); + } + } + @Test + void inactiveClosureCaptureIsNotRetiredByScopeBoundaryReachabilityCorrection() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash guard = new RuntimeHash(); + guard.blessId = NameNormalizer.getBlessId("Issue1649::CapturedScopeGuard"); + RuntimeScalar pad = new RuntimeScalar(); + pad.set(guard.createAnonymousReference()); + RuntimeScalar weak = new RuntimeScalar(); + weak.set(pad); + WeakRefRegistry.weaken(weak); + RuntimeCode code = new RuntimeCode((args, context) -> new RuntimeList(), null); + code.refCount = 0; + code.capturedScalars = new RuntimeScalar[]{pad}; + code.capturedScalarOwnerSlots = new PerlOwnerSlot[]{pad.retainClosureCaptureSlot()}; + RuntimeScalar owner = new RuntimeScalar(); + owner.set(new RuntimeScalar(code)); + RuntimeScalar.scopeExitCleanup(pad); + MortalList.flushAboveMark(); + assertEquals(1, pad.captureCount); + assertEquals(RuntimeScalarType.HASHREFERENCE, weak.type); + assertTrue(runtime.lifecycleState.deferredScopeExitCaptureCandidates.isEmpty()); + owner.set(RuntimeScalarCache.scalarUndef); + assertEquals(0, pad.captureCount); + assertEquals(RuntimeScalarType.UNDEF, weak.type); + } + } + +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/ActiveLexicalAggregateOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/ActiveLexicalAggregateOwnerSlotTest.java new file mode 100644 index 0000000000..8291183c03 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/ActiveLexicalAggregateOwnerSlotTest.java @@ -0,0 +1,64 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class ActiveLexicalAggregateOwnerSlotTest { + @Test + void activeArrayAndHashPadsOwnAndReleaseAggregateSlotsWithoutSweeps() { + Assumptions.assumeFalse(ReachabilityWalker.correctiveSweepsEnabled(), + "this lifecycle regression requires corrective sweeps to be disabled"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + MortalList.setActive(true); + RuntimeCode code = new RuntimeCode((args, context) -> new RuntimeList(), null); + RuntimeCode.pushActiveCode(code); + int mark = MyVarCleanupStack.pushMark(); + RuntimeArray array = new RuntimeArray(); + RuntimeHash hash = new RuntimeHash(); + RuntimeScalar weakArray = weakReferenceTo(array); + RuntimeScalar weakHash = weakReferenceTo(hash); + try { + MyVarCleanupStack.register(array); + MyVarCleanupStack.register(hash); + RuntimeCode.registerCurrentActiveLexical("@pad", array); + RuntimeCode.registerCurrentActiveLexical("%pad", hash); + WeakRefRegistry.weaken(weakArray); + WeakRefRegistry.weaken(weakHash); + + assertEquals(1, array.ownerSlotCount(PerlOwnerSlot.Kind.ACTIVE_LEXICAL)); + assertEquals(1, hash.ownerSlotCount(PerlOwnerSlot.Kind.ACTIVE_LEXICAL)); + assertTrue(WeakRefRegistry.isweak(weakArray)); + assertTrue(WeakRefRegistry.isweak(weakHash)); + + MortalList.scopeExitCleanupArray(array); + MyVarCleanupStack.unregister(array); + assertEquals(0, array.ownerSlotCount(PerlOwnerSlot.Kind.ACTIVE_LEXICAL)); + assertFalse(WeakRefRegistry.isweak(weakArray), + "the final array-pad release clears its weak observer immediately"); + + MortalList.scopeExitCleanupHash(hash); + MyVarCleanupStack.unregister(hash); + assertEquals(0, hash.ownerSlotCount(PerlOwnerSlot.Kind.ACTIVE_LEXICAL)); + assertFalse(WeakRefRegistry.isweak(weakHash), + "the final hash-pad release clears its weak observer immediately"); + } finally { + MyVarCleanupStack.unregister(array); + MyVarCleanupStack.unregister(hash); + MyVarCleanupStack.popMark(mark); + RuntimeCode.popActiveCode(code); + if (WeakRefRegistry.isweak(weakArray)) WeakRefRegistry.unweaken(weakArray); + if (WeakRefRegistry.isweak(weakHash)) WeakRefRegistry.unweaken(weakHash); + } + } + } + + private static RuntimeScalar weakReferenceTo(RuntimeBase target) { + return target.createReference(); + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/ArgumentAliasHandoffOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/ArgumentAliasHandoffOwnerSlotTest.java new file mode 100644 index 0000000000..ded350021d --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/ArgumentAliasHandoffOwnerSlotTest.java @@ -0,0 +1,133 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +@Tag("unit") +class ArgumentAliasHandoffOwnerSlotTest { + @Test + void queuedArgumentReleaseKeepsExplicitFrameIdentityAcrossCopyButNotThreadClone() { + PerlRuntime sourceRuntime = new PerlRuntime(); + PerlRuntime childRuntime = new PerlRuntime(); + RuntimeScalar sourceArgument = new RuntimeScalar(7); + RuntimeHash pendingBase = new RuntimeHash(); + RuntimeScalar copiedArgument; + Object sourceFrame; + boolean frameActive = false; + + try (PerlRuntime.Binding ignored = sourceRuntime.bind()) { + RuntimeArray arguments = new RuntimeArray(); + arguments.elements.add(sourceArgument); + RuntimeCode.pushArgs(arguments); + frameActive = true; + sourceFrame = RuntimeCode.currentArgumentAliasFrame(sourceArgument); + + copiedArgument = new RuntimeScalar(sourceArgument); + assertSame(sourceFrame, + RuntimeCode.argumentAliasFrameForOwner(copiedArgument)); + + MortalList.deferDecrementFromArraySlot( + pendingBase, copiedArgument, "argument-frame identity test"); + assertSame(sourceFrame, sourceRuntime.lifecycleState.pendingArgumentFrames.get(0), + "the queue records the originating frame identity at enqueue time"); + + RuntimeCode.popArgs(); + frameActive = false; + + RuntimeScalar clonedArgument = (RuntimeScalar) new RuntimeGraphCloner( + sourceRuntime, childRuntime).cloneGraph(copiedArgument); + try (PerlRuntime.Binding childBinding = childRuntime.bind()) { + assertNull(RuntimeCode.argumentAliasFrameForOwner(clonedArgument), + "a cloned scalar cannot retain a parent thread's argument frame"); + } + MortalList.clearCurrentRuntimeState(); + } finally { + if (frameActive) { + try (PerlRuntime.Binding ignored = sourceRuntime.bind()) { + RuntimeCode.popArgs(); + MortalList.clearCurrentRuntimeState(); + } + } + } + } + + @Test + void tailCallTargetFrameGetsAndReleasesItsOwnAliasIdentity() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar cell = new RuntimeScalar(); + cell.isPackageGlobalRoot = true; + cell.set(referent.createAnonymousReference()); + int originalRefCount = referent.refCount; + + RuntimeArray sourceArgs = new RuntimeArray(); + sourceArgs.elements.add(cell); + RuntimeArray targetArgs = new RuntimeArray(); + targetArgs.elements.add(cell); + boolean sourceFrameActive = false; + boolean targetFrameActive = false; + try { + RuntimeCode.pushArgs(sourceArgs); + sourceFrameActive = true; + RuntimeCode.pushArgs(targetArgs); + targetFrameActive = true; + + assertEquals(2, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS), + "the tail-call target owns a separate active-frame identity"); + assertEquals(originalRefCount, referent.refCount, + "frame identities do not change Perl-visible reference counts"); + + RuntimeCode.popArgs(); + targetFrameActive = false; + assertEquals(1, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS)); + + RuntimeCode.popArgs(); + sourceFrameActive = false; + assertEquals(0, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS)); + } finally { + if (targetFrameActive) RuntimeCode.popArgs(); + if (sourceFrameActive) RuntimeCode.popArgs(); + } + } + } + + @Test + void sharedScalarArgumentRetiresItsAliasIdentityOnOverwriteAndFrameExit() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash sharedReferent = new RuntimeHash(); + sharedReferent.threadShared = true; + sharedReferent.threadSharedIdentity = new Object(); + RuntimeScalar sharedCell = new RuntimeScalar(); + sharedCell.localBindingExists = true; + sharedCell.set(sharedReferent.createAnonymousReference()); + sharedCell.threadShared = true; + sharedCell.threadSharedIdentity = new Object(); + + RuntimeArray args = new RuntimeArray(); + args.elements.add(sharedCell); + boolean argsActive = false; + try { + RuntimeCode.pushArgs(args); + argsActive = true; + assertEquals(1, sharedReferent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS)); + + sharedCell.set(new RuntimeScalar(1)); + assertEquals(0, sharedReferent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS), + "a shared cell transfers its frame identity when its value changes"); + + RuntimeCode.popArgs(); + argsActive = false; + assertEquals(0, sharedReferent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS), + "frame exit does not release the transferred identity twice"); + } finally { + if (argsActive) RuntimeCode.popArgs(); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/ArgumentAliasOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/ArgumentAliasOwnerSlotTest.java new file mode 100644 index 0000000000..5ff2a19ae9 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/ArgumentAliasOwnerSlotTest.java @@ -0,0 +1,293 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class ArgumentAliasOwnerSlotTest { + @Test + void removingOneDuplicateArgumentSlotRetiresOnlyItsFrameIdentity() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash target = new RuntimeHash(); + RuntimeScalar cell = new RuntimeScalar(); + cell.isPackageGlobalRoot = true; + cell.set(target.createAnonymousReference()); + RuntimeArray args = new RuntimeArray(); + args.elements.add(cell); + args.elements.add(cell); + boolean argsActive = false; + try { + RuntimeCode.pushArgs(args); + argsActive = true; + + assertEquals(2, target.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS)); + assertSame(cell, RuntimeArray.shift(args)); + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS), + "removing one duplicate @_ slot retires exactly one frame identity"); + + assertSame(cell, RuntimeArray.pop(args)); + assertEquals(0, target.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS), + "removing the last duplicate retires its remaining identity"); + + RuntimeCode.popArgs(); + argsActive = false; + assertEquals(0, target.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS)); + } finally { + if (argsActive) RuntimeCode.popArgs(); + } + } + } + + @Test + void replacingAndClearingArgumentSlotsRetiresTheirFrameIdentities() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash first = new RuntimeHash(); + RuntimeHash second = new RuntimeHash(); + RuntimeScalar firstCell = new RuntimeScalar(); + firstCell.isPackageGlobalRoot = true; + firstCell.set(first.createAnonymousReference()); + RuntimeScalar secondCell = new RuntimeScalar(); + secondCell.isPackageGlobalRoot = true; + secondCell.set(second.createAnonymousReference()); + RuntimeArray args = new RuntimeArray(); + args.elements.add(firstCell); + args.elements.add(secondCell); + boolean argsActive = false; + try { + RuntimeCode.pushArgs(args); + argsActive = true; + + args.elements.set(0, new RuntimeScalar()); + assertEquals(0, first.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS)); + assertEquals(1, second.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS)); + + args.elements.clear(); + assertEquals(0, second.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS)); + + RuntimeCode.popArgs(); + argsActive = false; + assertEquals(0, first.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS)); + assertEquals(0, second.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS)); + } finally { + if (argsActive) RuntimeCode.popArgs(); + } + } + } + + @Test + void stagedAndActiveArgumentAliasesTrackCellAssignmentsWithoutChangingRefCount() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash first = new RuntimeHash(); + RuntimeHash second = new RuntimeHash(); + RuntimeScalar cell = new RuntimeScalar(); + cell.isPackageGlobalRoot = true; + RuntimeCode.CallArguments staged = RuntimeCode.newCallArguments(1); + RuntimeArray args = null; + boolean argsActive = false; + + try { + cell.set(first.createAnonymousReference()); + int initialRuntimeRoots = runtime.positiveOwnerSlotReferentCount(first); + int firstRefCount = first.refCount; + staged.add(cell); + args = staged.toRuntimeArray(); + RuntimeCode.pushArgs(args); + argsActive = true; + + assertEquals(firstRefCount, first.refCount, + "argument alias identities must not add Perl-visible references"); + assertEquals(initialRuntimeRoots, runtime.positiveOwnerSlotReferentCount(first), + "an active alias frame must not register another runtime root"); + assertEquals(2, first.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS), + "staging and active @_ each have their own frame identity"); + + cell.set(new RuntimeScalar(7)); + assertEquals(0, first.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS), + "assigning a primitive releases the old referent identities"); + + RuntimeScalar secondReference = second.createAnonymousReference(); + int secondRefCountBeforeStore = second.refCount; + cell.set(secondReference); + assertEquals(2, second.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS), + "a primitive argument cell can acquire a later reference value"); + assertEquals(secondRefCountBeforeStore + 1, second.refCount, + "the two alias identities do not add to the normal scalar-store count"); + + cell.set(new RuntimeScalar()); + assertEquals(0, second.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS)); + } finally { + if (argsActive) RuntimeCode.popArgs(); + staged.finish(); + } + } + } + + @Test + void overwrittenOwnerWaitsForAllArgumentAliasFramesBeforeFinalRelease() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + MortalList.setActive(true); + MortalList.pushMark(); + boolean markActive = true; + RuntimeHash target = new RuntimeHash(); + target.blessId = NameNormalizer.getBlessId("Issue1649::ArgumentAlias"); + RuntimeScalar released = new RuntimeScalar(); + released.set(target.createAnonymousReference()); + RuntimeScalar argumentCell = new RuntimeScalar(); + argumentCell.isPackageGlobalRoot = true; + argumentCell.set(target.createAnonymousReference()); + PerlOwnerSlot argumentCellOwner = argumentCell.countedOwnerSlot(); + argumentCellOwner.release(); + argumentCell.refCountOwned = false; + target.refCount = 1; // The released scalar is the only counted owner. + + RuntimeScalar weak = target.createAnonymousReference(); + WeakRefRegistry.weaken(weak); + RuntimeCode.CallArguments staged = RuntimeCode.newCallArguments(1); + RuntimeArray args = null; + boolean argsActive = false; + try { + staged.add(argumentCell); + args = staged.toRuntimeArray(); + RuntimeCode.pushArgs(args); + argsActive = true; + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + released.set(new RuntimeScalar()); + MortalList.flushAboveMark(); + + assertEquals(1, target.refCount); + assertEquals(2, target.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS)); + assertTrue(WeakRefRegistry.isweak(weak)); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + RuntimeCode.popArgs(); + argsActive = false; + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS), + "the staged call identity remains until CallArguments.finish()"); + assertTrue(WeakRefRegistry.isweak(weak)); + + staged.finish(); + assertEquals(0, target.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS)); + assertEquals(RuntimeScalarType.UNDEF, weak.type); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, + target.perlLifecycleState()); + + MortalList.popAndFlush(); + markActive = false; + } finally { + if (argsActive) RuntimeCode.popArgs(); + staged.finish(); + if (markActive) MortalList.popAndFlush(); + if (WeakRefRegistry.isweak(weak)) WeakRefRegistry.unweaken(weak); + } + } + } + + @Test + void explicitUndefKeepsAWeaklyObservedTargetUntilArgumentAliasesExit() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + MortalList.setActive(true); + MortalList.pushMark(); + boolean markActive = true; + RuntimeHash target = new RuntimeHash(); + target.blessId = NameNormalizer.getBlessId("Issue1649::ArgumentAliasUndef"); + RuntimeScalar released = new RuntimeScalar(); + released.set(target.createAnonymousReference()); + RuntimeScalar argumentCell = new RuntimeScalar(); + argumentCell.isPackageGlobalRoot = true; + argumentCell.set(target.createAnonymousReference()); + argumentCell.countedOwnerSlot().release(); + argumentCell.refCountOwned = false; + target.refCount = 1; + + RuntimeScalar weak = target.createAnonymousReference(); + WeakRefRegistry.weaken(weak); + RuntimeCode.CallArguments staged = RuntimeCode.newCallArguments(1); + RuntimeArray args = null; + boolean argsActive = false; + try { + staged.add(argumentCell); + args = staged.toRuntimeArray(); + RuntimeCode.pushArgs(args); + argsActive = true; + + released.undefine(); + assertEquals(1, target.refCount); + assertTrue(WeakRefRegistry.isweak(weak)); + assertEquals(2, target.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS)); + + RuntimeCode.popArgs(); + argsActive = false; + assertTrue(WeakRefRegistry.isweak(weak)); + staged.finish(); + + assertEquals(RuntimeScalarType.UNDEF, weak.type); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, + target.perlLifecycleState()); + MortalList.popAndFlush(); + markActive = false; + } finally { + if (argsActive) RuntimeCode.popArgs(); + staged.finish(); + if (markActive) MortalList.popAndFlush(); + if (WeakRefRegistry.isweak(weak)) WeakRefRegistry.unweaken(weak); + } + } + } + + @Test + void transientArgumentValuesDoNotGetPersistentCellIdentities() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash target = new RuntimeHash(); + RuntimeScalar transientValue = target.createAnonymousReference(); + RuntimeCode.CallArguments staged = RuntimeCode.newCallArguments(1); + RuntimeArray args = null; + boolean argsActive = false; + + try { + staged.add(transientValue); + args = staged.toRuntimeArray(); + RuntimeCode.pushArgs(args); + argsActive = true; + + assertEquals(0, target.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS), + "temporary expression wrappers are not caller storage cells"); + } finally { + if (argsActive) RuntimeCode.popArgs(); + staged.finish(); + } + } + } + + @Test + void staleCellReleasesAliasIdentityWhenReferentWasAlreadyDestroyed() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash target = new RuntimeHash(); + RuntimeScalar cell = new RuntimeScalar(); + cell.isPackageGlobalRoot = true; + cell.set(target.createAnonymousReference()); + RuntimeScalar.ArgumentAliasOwner alias = cell.retainArgumentAliasOwner(); + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS)); + + target.refCount = Integer.MIN_VALUE; + cell.refreshArgumentAliasOwnerSlots(); + + assertEquals(0, target.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_ALIAS)); + alias.release(); + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/CallArgumentsNestedAggregateOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/CallArgumentsNestedAggregateOwnerSlotTest.java new file mode 100644 index 0000000000..775e43a4a3 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/CallArgumentsNestedAggregateOwnerSlotTest.java @@ -0,0 +1,52 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Tag("unit") +class CallArgumentsNestedAggregateOwnerSlotTest { + @Test + void nestedListArgumentOwnsStrongArrayAndHashElementsUntilCallFinishes() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar source = new RuntimeScalar(); + source.set(referent.createAnonymousReference()); + RuntimeScalar arrayValue = RuntimeScalar.copyToMortalReturnSlot(source); + RuntimeScalar hashValue = RuntimeScalar.copyToMortalReturnSlot(source); + + RuntimeArray nestedArray = new RuntimeArray(); + nestedArray.elements.add(arrayValue); + RuntimeHash nestedHash = new RuntimeHash(); + nestedHash.elements.put("value", hashValue); + RuntimeList innerList = new RuntimeList(); + innerList.elements.add(nestedArray); + innerList.elements.add(nestedHash); + RuntimeList outerList = new RuntimeList(); + outerList.elements.add(innerList); + + RuntimeCode.CallArguments arguments = RuntimeCode.newCallArguments(2); + try { + arguments.add(outerList); + arguments.add(new RuntimeScalar(1)); + + assertEquals(2, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE), + "nested aggregate elements each get a staged argument owner"); + + MortalList.flushAboveMark(); + assertEquals(0, referent.ownerSlotCount(PerlOwnerSlot.Kind.RETURN_VALUE)); + assertEquals(2, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE), + "later argument cleanup must preserve both aggregate elements"); + + arguments.finish(); + MortalList.flushAboveMark(); + assertEquals(0, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE), + "call completion retires every staged aggregate owner"); + } finally { + arguments.finish(); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/CallArgumentsScalarStagingOwnerTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/CallArgumentsScalarStagingOwnerTest.java new file mode 100644 index 0000000000..893e379f4f --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/CallArgumentsScalarStagingOwnerTest.java @@ -0,0 +1,92 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class CallArgumentsScalarStagingOwnerTest { + @Test + void mortalScalarArgumentOwnsItsReferentAcrossLaterArgumentCleanup() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar source = new RuntimeScalar(); + source.set(referent.createAnonymousReference()); + RuntimeScalar temporary = RuntimeScalar.copyToMortalReturnSlot(source); + RuntimeCode.CallArguments arguments = RuntimeCode.newCallArguments(2); + + arguments.add(temporary); + arguments.add(new RuntimeScalar(1)); + + assertTrue(temporary.isMortalReturnSnapshot()); + assertEquals(1, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + assertEquals(RuntimeBase.PerlLifecycleState.LIVE, referent.perlLifecycleState()); + + // A later argument expression can drain the return owner before + // the call has materialized its @_ aliases. + MortalList.flushAboveMark(); + assertEquals(0, referent.ownerSlotCount(PerlOwnerSlot.Kind.RETURN_VALUE)); + assertEquals(1, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + assertEquals(RuntimeBase.PerlLifecycleState.LIVE, referent.perlLifecycleState()); + + arguments.finish(); + MortalList.flushAboveMark(); + + assertEquals(0, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + assertEquals(0, runtime.positiveOwnerSlotReferentCount(referent)); + assertTrue(source.countedOwnerSlot().isActive()); + } + } + + @Test + void persistentScalarArgumentContinuesToBorrowItsCallerCell() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar source = new RuntimeScalar(); + source.set(referent.createAnonymousReference()); + RuntimeCode.CallArguments arguments = RuntimeCode.newCallArguments(2); + + arguments.add(source); + + assertEquals(1, referent.refCount); + assertEquals(0, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + + arguments.finish(); + assertEquals(1, referent.refCount); + assertTrue(source.countedOwnerSlot().isActive()); + } + } + + @Test + void finalMortalScalarArgumentSurvivesCalleeCleanup() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar borrowedSource = new RuntimeScalar(referent.createAnonymousReference()); + assertFalse(borrowedSource.refCountOwned); + RuntimeScalar temporary = RuntimeScalar.copyToMortalReturnSlot(borrowedSource); + RuntimeCode.CallArguments arguments = RuntimeCode.newCallArguments(1); + + arguments.add(temporary); + + assertEquals(0, referent.ownerSlotCount(PerlOwnerSlot.Kind.RETURN_VALUE)); + assertEquals(1, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + + // Safe cleanup inside the callee must preserve the staged owner. + MortalList.flushAboveMark(); + assertEquals(0, referent.ownerSlotCount(PerlOwnerSlot.Kind.RETURN_VALUE)); + assertEquals(1, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + assertEquals(RuntimeBase.PerlLifecycleState.LIVE, referent.perlLifecycleState()); + + arguments.finish(); + MortalList.flushAboveMark(); + assertEquals(0, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + assertEquals(0, runtime.positiveOwnerSlotReferentCount(referent)); + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/CaptureOwnerReleaseWithoutWalkerTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/CaptureOwnerReleaseWithoutWalkerTest.java new file mode 100644 index 0000000000..1f6b1a0c29 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/CaptureOwnerReleaseWithoutWalkerTest.java @@ -0,0 +1,54 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +@Tag("unit") +class CaptureOwnerReleaseWithoutWalkerTest { + @Test + void explicitCodeOwnerLedgerControlsScopeExitedCaptureRelease() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray referent = new RuntimeArray(); + RuntimeScalar pad = referent.createAnonymousReference(); + pad.scopeExited = true; + + RuntimeCode closure = new RuntimeCode((args, context) -> new RuntimeList(), null); + closure.refCount = 1; + closure.capturedScalars = new RuntimeScalar[] {pad}; + closure.capturedScalarOwnerSlots = new PerlOwnerSlot[] { + pad.retainClosureCaptureSlot(closure) + }; + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + ReachabilityWalker.releaseUnreachableScopeExitedCaptureOwners( + List.of(referent), null); + + assertNotNull(closure.capturedScalars, + "a CODE owner with a Perl owner keeps its capture alive"); + assertEquals(0, measurement.stats().rootQueries, + "explicit CODE ownership requires no reachability walk"); + } + + closure.refCount = 0; + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + ReachabilityWalker.releaseUnreachableScopeExitedCaptureOwners( + List.of(referent), null); + + assertNull(closure.capturedScalars, + "an ownerless CODE releases its captures at scope exit"); + assertEquals(0, referent.semanticCaptureOwnerCount()); + assertEquals(0, measurement.stats().rootQueries, + "retiring an explicit capture owner requires no reachability walk"); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/CapturedScalarCellOwnerFastPathTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/CapturedScalarCellOwnerFastPathTest.java new file mode 100644 index 0000000000..a4c22207f5 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/CapturedScalarCellOwnerFastPathTest.java @@ -0,0 +1,112 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class CapturedScalarCellOwnerFastPathTest { + @Test + void activeClosureCaptureRepairsUnblessedScalarCellOwnerWithoutRootWalk() { + exerciseCaptureOwnerRepair(false); + } + + @Test + void deferredCleanupUsesActiveClosureCaptureWithoutRootWalk() { + exerciseCaptureOwnerRepair(true); + } + + private void exerciseCaptureOwnerRepair(boolean deferScalarRelease) { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + boolean previousMortalListState = MortalList.isActive(); + MortalList.setActive(deferScalarRelease); + try { + RuntimeHash target = new RuntimeHash(); + RuntimeScalar seedOwner = new RuntimeScalar(); + RuntimeScalar weakObserver = target.createAnonymousReference(); + RuntimeScalar capturedOwner = new RuntimeScalar(); + RuntimeScalar transientOwner = new RuntimeScalar(); + RuntimeCode closure = new RuntimeCode((args, context) -> new RuntimeList(), null); + int mark = MyVarCleanupStack.pushMark(); + MyVarCleanupStack.register(seedOwner); + MyVarCleanupStack.register(capturedOwner); + MyVarCleanupStack.register(transientOwner); + + try { + seedOwner.set(target.createAnonymousReference()); + WeakRefRegistry.weaken(weakObserver); + capturedOwner.set(target.createAnonymousReference()); + transientOwner.set(target.createAnonymousReference()); + closure.capturedScalars = new RuntimeScalar[]{capturedOwner}; + closure.capturedScalarOwnerSlots = new PerlOwnerSlot[]{ + capturedOwner.retainClosureCaptureSlot(closure) + }; + RuntimeCode.pushActiveCode(closure); + RuntimeScalar.scopeExitCleanup(capturedOwner); + MyVarCleanupStack.unregister(capturedOwner); + seedOwner.set(new RuntimeScalar()); + + assertFalse(target.localBindingExists); + assertEquals(PerlOwnerSlot.Kind.SCALAR_CELL, + capturedOwner.countedOwnerSlot().kind()); + assertTrue(capturedOwner.refCountOwned, + "the captured scalar cell still owns its reference count"); + assertTrue(capturedOwner.countedOwnerSlot().isActive(), + "the captured scalar cell's owner identity is active"); + assertSame(target, capturedOwner.countedOwnerSlot().referent()); + assertSame(capturedOwner, capturedOwner.countedOwnerSlot().countedScalarOwner()); + assertEquals(1, capturedOwner.captureCount); + assertEquals(1, capturedOwner.ownerSlotCount(PerlOwnerSlot.Kind.SCALAR_CAPTURE)); + assertTrue(closure.hasPerlOwnerForCapture(), + "the closure remains a Perl owner of the captured scalar cell"); + assertTrue(capturedOwner.isCurrentCapturedScalarCellOwner( + target, capturedOwner.countedOwnerSlot()), + "the exact scalar-cell and closure-capture identities prove ownership"); + + // Model a selective-count undercount while preserving the + // active closure's scalar-cell owner identity. + target.refCount = 1; + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + transientOwner.set(new RuntimeScalar()); + + assertEquals(1, target.refCount); + assertTrue(WeakRefRegistry.isweak(weakObserver)); + assertEquals(0, measurement.stats().scalarReachabilityQueries, + "the active capture identity is the liveness proof"); + assertEquals(0, measurement.stats().scalarReachabilityRootWalks); + assertEquals(0, measurement.stats().rootQueries); + } + + MortalList.setActive(true); + RuntimeCode.popActiveCode(closure); + closure.releaseCaptures(); + MortalList.flush(); + assertFalse(WeakRefRegistry.isweak(weakObserver), + "releasing the captured scalar cell clears the final weak observer"); + assertEquals(RuntimeScalarType.UNDEF, weakObserver.type); + } finally { + if (RuntimeCode.isActiveCode(closure)) RuntimeCode.popActiveCode(closure); + if (closure.capturedScalars != null) closure.releaseCaptures(); + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + RuntimeScalar.scopeExitCleanup(seedOwner); + RuntimeScalar.scopeExitCleanup(capturedOwner); + RuntimeScalar.scopeExitCleanup(transientOwner); + MyVarCleanupStack.unregister(seedOwner); + MyVarCleanupStack.unregister(capturedOwner); + MyVarCleanupStack.unregister(transientOwner); + MyVarCleanupStack.popMark(mark); + } + } finally { + MortalList.setActive(previousMortalListState); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/ClearedAggregateOwnerIdentityFinalizationTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/ClearedAggregateOwnerIdentityFinalizationTest.java new file mode 100644 index 0000000000..75fe391a20 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/ClearedAggregateOwnerIdentityFinalizationTest.java @@ -0,0 +1,113 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class ClearedAggregateOwnerIdentityFinalizationTest { + @Test + void scopeExitUsesPackageOwnerIdentityWithoutExternalRootWalk() { + PerlRuntime runtime = new PerlRuntime(); + RuntimeHash packageRoot = new RuntimeHash(); + RuntimeScalar weakPackageRoot = packageRoot.createAnonymousReference(); + String globalName = + "ClearedAggregateOwnerIdentityFinalizationTest::packageRoot"; + + try (PerlRuntime.Binding ignored = runtime.bind()) { + boolean previousMortalListState = MortalList.isActive(); + MortalList.setActive(true); + try { + GlobalVariable.globalHashes.put(globalName, packageRoot); + assertEquals(1, packageRoot.ownerSlotCount( + PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT)); + packageRoot.clearedOwnedAggregateElement = true; + WeakRefRegistry.weaken(weakPackageRoot); + packageRoot.refCount = 0; + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + MortalList.finalizeClearedAggregateOwnerAfterScopeExit(packageRoot); + + assertEquals(0, measurement.stats().rootQueries, + "the package aggregate owner identity replaces the root walk"); + assertEquals(0, measurement.stats().snapshotsBuilt); + assertTrue(WeakRefRegistry.isweak(weakPackageRoot), + "the package slot keeps the aggregate alive"); + } + + } finally { + GlobalVariable.globalHashes.remove(globalName); + WeakRefRegistry.clearWeakRefsTo(packageRoot); + MortalList.invalidateAllRootSnapshots(); + MortalList.setActive(previousMortalListState); + } + } + } + + @Test + void finalCellCleanupReleasesNestedUntrackedOwnerDespiteLegacyCount() { + PerlRuntime runtime = new PerlRuntime(); + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar weakObserver = referenceTo(referent); + RuntimeArray container = new RuntimeArray(); + RuntimeScalar nestedCell = referenceTo(referent); + container.elements.add(nestedCell); + nestedCell.markContainerOwner(container); + + try (PerlRuntime.Binding ignored = runtime.bind()) { + boolean previousMortalListState = MortalList.isActive(); + int mark = MyVarCleanupStack.pushMark(); + MyVarCleanupStack.register(container); + MortalList.setActive(true); + try { + referent.refCount = -1; + nestedCell.refreshUntrackedReferenceOwner(); + PerlOwnerSlot nestedOwner = nestedCell.untrackedReferenceOwnerSlot(); + assertTrue(nestedOwner != null && nestedOwner.isActive()); + + WeakRefRegistry.weaken(weakObserver); + assertTrue(WeakRefRegistry.isweak(weakObserver)); + + // Model a positive compatibility count left behind after the + // nested scalar's final untracked owner identity is retired. + referent.refCount = 5; + referent.clearedOwnedAggregateElement = true; + nestedCell.set(new RuntimeScalar()); + assertFalse(nestedOwner.isActive()); + assertEquals(0, referent.ownerSlotCount( + PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + MortalList.finalizeClearedAggregateOwnerAfterScopeExit(referent); + + assertFalse(WeakRefRegistry.isweak(weakObserver), + "legacy count alone must not keep a cleared aggregate alive"); + assertEquals(Integer.MIN_VALUE, referent.refCount); + } finally { + if (nestedCell.untrackedReferenceOwnerSlot() != null + && nestedCell.untrackedReferenceOwnerSlot().isActive()) { + nestedCell.set(new RuntimeScalar()); + } + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + WeakRefRegistry.clearWeakRefsTo(referent); + MortalList.scopeExitCleanupArray(container); + MyVarCleanupStack.unregister(container); + MyVarCleanupStack.popMark(mark); + MortalList.invalidateAllRootSnapshots(); + MortalList.setActive(previousMortalListState); + } + } + } + + private static RuntimeScalar referenceTo(RuntimeBase referent) { + RuntimeScalar scalar = new RuntimeScalar(); + scalar.type = RuntimeScalarType.HASHREFERENCE; + scalar.value = referent; + return scalar; + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/ClosureAggregateOwnerIdentityReleaseTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/ClosureAggregateOwnerIdentityReleaseTest.java new file mode 100644 index 0000000000..501e2e73bb --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/ClosureAggregateOwnerIdentityReleaseTest.java @@ -0,0 +1,75 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class ClosureAggregateOwnerIdentityReleaseTest { + @Test + void scopeExitReleasesCaptureDespiteStaleCodeRefCountWithoutWalkingRoots() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray capturedPad = new RuntimeArray(); + RuntimeCode closure = new RuntimeCode(null, List.of()); + PerlOwnerSlot padOwner = capturedPad.retainClosureCaptureOwner(closure); + closure.capturedAggregates = new RuntimeBase[] {capturedPad}; + closure.capturedAggregateOwnerSlots = new PerlOwnerSlot[] {padOwner}; + closure.captureFieldsRecorded = true; + + RuntimeScalar weakCode = new RuntimeScalar(closure); + WeakRefRegistry.weaken(weakCode); + closure.refCount = 7; // stale compatibility count, with no strong owner slot + assertFalse(closure.hasPerlOwnerForCaptureOtherThan(null)); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + MortalList.scopeExitCleanupArray(capturedPad); + + assertNull(closure.capturedAggregates); + assertNull(closure.capturedAggregateOwnerSlots); + assertFalse(padOwner.isActive()); + assertEquals(0, capturedPad.captureCount); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } finally { + WeakRefRegistry.removeWeakRef(weakCode, closure); + MortalList.invalidateAllRootSnapshots(); + } + } + } + + @Test + void scopeExitKeepsCaptureWhileCodeHasAScalarOwnerIdentity() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray capturedPad = new RuntimeArray(); + RuntimeCode closure = new RuntimeCode(null, List.of()); + closure.refCount = 0; + PerlOwnerSlot padOwner = capturedPad.retainClosureCaptureOwner(closure); + closure.capturedAggregates = new RuntimeBase[] {capturedPad}; + closure.capturedAggregateOwnerSlots = new PerlOwnerSlot[] {padOwner}; + closure.captureFieldsRecorded = true; + closure.capturedScalars = new RuntimeScalar[0]; + RuntimeScalar codeOwner = new RuntimeScalar(); + codeOwner.set(new RuntimeScalar(closure)); + + assertTrue(closure.hasPerlOwnerForCaptureOtherThan(null)); + MortalList.scopeExitCleanupArray(capturedPad); + + assertTrue(padOwner.isActive()); + assertEquals(1, capturedPad.captureCount); + assertTrue(capturedPad.scopeExited); + + codeOwner.set(new RuntimeScalar()); + assertNull(closure.capturedAggregates); + assertFalse(padOwner.isActive()); + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/CodeOwnerSlotReleaseFastPathTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/CodeOwnerSlotReleaseFastPathTest.java new file mode 100644 index 0000000000..3838f2371d --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/CodeOwnerSlotReleaseFastPathTest.java @@ -0,0 +1,47 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +@Tag("unit") +class CodeOwnerSlotReleaseFastPathTest { + @Test + void anotherScalarOwnerSkipsClosureCaptureRootWalk() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeCode closure = new RuntimeCode((args, context) -> new RuntimeList(), null); + closure.refCount = 0; + RuntimeScalar pad = new RuntimeScalar(); + closure.capturedScalars = new RuntimeScalar[] {pad}; + closure.capturedScalarOwnerSlots = new PerlOwnerSlot[] { + pad.retainClosureCaptureSlot(closure) + }; + + RuntimeScalar external = new RuntimeScalar(); + external.set(new RuntimeScalar(closure)); + RuntimeScalar local = new RuntimeScalar(); + local.set(new RuntimeScalar(closure)); + assertEquals(2, closure.ownerSlotCount(PerlOwnerSlot.Kind.SCALAR_CELL)); + assertEquals(0, closure.ownerSlotCount(PerlOwnerSlot.Kind.CODE_SLOT)); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + local.set(new RuntimeScalar()); + + assertNotNull(closure.capturedScalars, + "the other scalar owner keeps closure captures alive"); + assertEquals(1, closure.ownerSlotCount(PerlOwnerSlot.Kind.SCALAR_CELL)); + assertEquals(0, measurement.stats().rootQueries, + "an explicit owner slot replaces the closure-root query"); + } + + external.set(new RuntimeScalar()); + assertNull(closure.capturedScalars, + "the final scalar owner still releases closure captures"); + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/CodePadConstantOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/CodePadConstantOwnerSlotTest.java new file mode 100644 index 0000000000..25dba2b7e5 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/CodePadConstantOwnerSlotTest.java @@ -0,0 +1,246 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class CodePadConstantOwnerSlotTest { + @Test + void compiledPadTreeTransfersToThePublishedCvOwner() { + PerlRuntime runtime = new PerlRuntime(); + RuntimeScalarReadOnly constant = new RuntimeScalarReadOnly("owned"); + RuntimeCode compiledCode = new RuntimeCode((args, context) -> new RuntimeList(), null); + RuntimeCode publishedCv = new RuntimeCode((args, context) -> new RuntimeList(), null); + RuntimeScalar weakObserver = referenceTo(constant); + String className = "org/perlonjava/test/PadConstantTransfer"; + + try (PerlRuntime.Binding ignored = runtime.bind()) { + boolean previousMortalListState = MortalList.isActive(); + MortalList.setActive(true); + try { + WeakRefRegistry.weaken(weakObserver); + compiledCode.installPadConstantTree( + className, new RuntimeBase[]{constant}, java.util.Map.of(), java.util.Map.of()); + PerlOwnerSlot compiledOwner = constant.ownerSlotSnapshot().stream() + .filter(slot -> slot.kind() == PerlOwnerSlot.Kind.CODE_PAD_CONSTANT) + .findFirst().orElseThrow(); + + compiledCode.transferPadConstantTreeTo(publishedCv); + + assertFalse(compiledOwner.isActive(), + "the temporary compiler wrapper gives up its owner slot"); + assertEquals(1, constant.ownerSlotCount(PerlOwnerSlot.Kind.CODE_PAD_CONSTANT), + "the published CV replaces the temporary owner exactly once"); + assertSame(constant, RuntimeCode.getPadConstant(className, 0), + "generated code resolves the constant through the published CV tree"); + assertTrue(WeakRefRegistry.isweak(weakObserver), + "transferring the owner does not clear a live constant"); + + publishedCv.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroy(publishedCv); + assertEquals(0, constant.ownerSlotCount(PerlOwnerSlot.Kind.CODE_PAD_CONSTANT)); + assertFalse(WeakRefRegistry.isweak(weakObserver), + "the published CV's final release reaps its constant"); + } finally { + if (compiledCode.perlLifecycleState() != RuntimeBase.PerlLifecycleState.DESTROYED) { + compiledCode.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroy(compiledCode); + } + if (publishedCv.perlLifecycleState() != RuntimeBase.PerlLifecycleState.DESTROYED) { + publishedCv.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroy(publishedCv); + } + WeakRefRegistry.clearWeakRefsTo(constant); + MortalList.invalidateAllRootSnapshots(); + MortalList.setActive(previousMortalListState); + } + } + } + + @Test + void untrackedCallerOwnerIsFinalizedAfterItsCvReleasesThePadConstant() { + PerlRuntime runtime = new PerlRuntime(); + RuntimeScalarReadOnly constant = new RuntimeScalarReadOnly("owned"); + RuntimeCode code = new RuntimeCode((args, context) -> new RuntimeList(), null); + RuntimeScalar strongOwner = new RuntimeScalar(); + RuntimeScalar weakObserver = referenceTo(constant); + + try (PerlRuntime.Binding ignored = runtime.bind()) { + boolean previousMortalListState = MortalList.isActive(); + MortalList.setActive(true); + int mark = MyVarCleanupStack.pushMark(); + MyVarCleanupStack.register(strongOwner); + try { + code.installPadConstants(new RuntimeBase[]{constant}); + strongOwner.set(referenceTo(constant)); + assertTrue(strongOwner.untrackedReferenceOwnerSlot().isActive()); + WeakRefRegistry.weaken(weakObserver); + + code.clearPadConstantWeakRefs(); + + assertEquals(0, constant.ownerSlotCount(PerlOwnerSlot.Kind.CODE_PAD_CONSTANT)); + assertEquals(1, constant.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertTrue(WeakRefRegistry.isweak(weakObserver), + "the caller's strong scalar keeps the constant alive after CV release"); + + strongOwner.undefine(); + + assertEquals(0, constant.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertFalse(WeakRefRegistry.isweak(weakObserver), + "the final untracked caller slot triggers immediate weak cleanup"); + } finally { + RuntimeScalar.scopeExitCleanup(strongOwner); + MyVarCleanupStack.unregister(strongOwner); + MyVarCleanupStack.popMark(mark); + if (code.perlLifecycleState() != RuntimeBase.PerlLifecycleState.DESTROYED) { + code.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroy(code); + } + WeakRefRegistry.clearWeakRefsTo(constant); + MortalList.invalidateAllRootSnapshots(); + MortalList.setActive(previousMortalListState); + } + } + } + + @Test + void finalUntrackedCodeReferenceReleasesItsPadTreeDespiteStaleLegacyCount() { + PerlRuntime runtime = new PerlRuntime(); + RuntimeScalarReadOnly constant = new RuntimeScalarReadOnly("owned"); + RuntimeCode closure = new RuntimeCode((args, context) -> new RuntimeList(), null); + RuntimeScalar codeSlot = new RuntimeScalar(); + RuntimeScalar weakObserver = referenceTo(constant); + + try (PerlRuntime.Binding ignored = runtime.bind()) { + boolean previousMortalListState = MortalList.isActive(); + MortalList.setActive(true); + int mark = MyVarCleanupStack.pushMark(); + MyVarCleanupStack.register(codeSlot); + try { + closure.installPadConstants(new RuntimeBase[]{constant}); + codeSlot.set(new RuntimeScalar(closure)); + WeakRefRegistry.weaken(weakObserver); + + assertEquals(1, + closure.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertEquals(1, + constant.ownerSlotCount(PerlOwnerSlot.Kind.CODE_PAD_CONSTANT)); + // The scalar slot is the actual CODE owner. Its old + // compatibility count can remain positive after that owner + // identity is the only remaining proof of Perl liveness. + closure.refCount = 7; + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + codeSlot.set(new RuntimeScalar()); + + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + assertEquals(0, + constant.ownerSlotCount(PerlOwnerSlot.Kind.CODE_PAD_CONSTANT)); + assertEquals(0, closure.padConstantOwnerReferents().length); + assertFalse(WeakRefRegistry.isweak(weakObserver), + "the final uncounted CV slot releases the optree constants"); + } finally { + RuntimeScalar.scopeExitCleanup(codeSlot); + MyVarCleanupStack.unregister(codeSlot); + MyVarCleanupStack.popMark(mark); + closure.clearPadConstantWeakRefs(); + WeakRefRegistry.clearWeakRefsTo(constant); + MortalList.invalidateAllRootSnapshots(); + MortalList.setActive(previousMortalListState); + } + } + } + + @Test + void eachOptreeConstantEdgeLivesUntilItsCvOwnerIsDestroyed() { + PerlRuntime runtime = new PerlRuntime(); + RuntimeScalarReadOnly constant = new RuntimeScalarReadOnly("owned"); + RuntimeCode firstCode = new RuntimeCode((args, context) -> new RuntimeList(), null); + RuntimeCode secondCode = new RuntimeCode((args, context) -> new RuntimeList(), null); + RuntimeScalar weakObserver = referenceTo(constant); + + try (PerlRuntime.Binding ignored = runtime.bind()) { + boolean previousMortalListState = MortalList.isActive(); + MortalList.setActive(true); + try { + firstCode.installPadConstants(new RuntimeBase[]{constant, constant}); + secondCode.installPadConstants(new RuntimeBase[]{constant}); + + PerlOwnerSlot[] owners = constant.ownerSlotSnapshot().stream() + .filter(slot -> slot.kind() == PerlOwnerSlot.Kind.CODE_PAD_CONSTANT) + .toArray(PerlOwnerSlot[]::new); + assertEquals(3, owners.length, + "each constant occurrence in each CV has a distinct strong slot"); + assertEquals(3, constant.ownerSlotCount(PerlOwnerSlot.Kind.CODE_PAD_CONSTANT)); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + WeakRefRegistry.weaken(weakObserver); + + assertTrue(WeakRefRegistry.isweak(weakObserver)); + assertTrue(RuntimeCode.isInstalledPadConstant(constant)); + assertEquals(0, measurement.stats().rootQueries, + "optree owner identities replace the global CODE scan"); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + // A returned reference contributes a real temporary count, + // but once it leaves the mortal boundary only the CODE pad + // slots remain. Preserve those slots without inventing a + // replacement refCount owner. + constant.refCount = 1; + MortalList.deferDecrement(constant); + MortalList.flush(); + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, constant.refCount, + "pad ownership defers cleanup without fabricating a counted owner"); + assertTrue(WeakRefRegistry.isweak(weakObserver)); + + firstCode.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroy(firstCode); + assertEquals(1, constant.ownerSlotCount(PerlOwnerSlot.Kind.CODE_PAD_CONSTANT)); + assertTrue(WeakRefRegistry.isweak(weakObserver), + "one surviving CV still owns the shared optree constant"); + + secondCode.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroy(secondCode); + assertEquals(0, constant.ownerSlotCount(PerlOwnerSlot.Kind.CODE_PAD_CONSTANT)); + assertFalse(WeakRefRegistry.isweak(weakObserver), + "the final CV release clears the constant's weak observers"); + assertEquals(RuntimeScalarType.UNDEF, weakObserver.type); + for (PerlOwnerSlot owner : owners) assertFalse(owner.isActive()); + + DestroyDispatch.callDestroy(secondCode); + assertEquals(0, constant.ownerSlotCount(PerlOwnerSlot.Kind.CODE_PAD_CONSTANT), + "repeated destruction cannot release an owner twice"); + } finally { + if (firstCode.perlLifecycleState() != RuntimeBase.PerlLifecycleState.DESTROYED) { + firstCode.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroy(firstCode); + } + if (secondCode.perlLifecycleState() != RuntimeBase.PerlLifecycleState.DESTROYED) { + secondCode.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroy(secondCode); + } + WeakRefRegistry.clearWeakRefsTo(constant); + MortalList.invalidateAllRootSnapshots(); + MortalList.setActive(previousMortalListState); + } + } + } + + private static RuntimeScalar referenceTo(RuntimeBase referent) { + RuntimeScalar scalar = new RuntimeScalar(); + scalar.type = RuntimeScalarType.REFERENCE; + scalar.value = referent; + return scalar; + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/CodeSlotOwnerTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/CodeSlotOwnerTest.java new file mode 100644 index 0000000000..36e0a2270d --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/CodeSlotOwnerTest.java @@ -0,0 +1,133 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class CodeSlotOwnerTest { + private static RuntimeCode code() { + return new RuntimeCode((args, context) -> new RuntimeList(), null); + } + + @Test + void compiledLookupPinOwnsTheGpAfterItsStashNameIsDeleted() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + RuntimeScalar slot = GlobalVariable.getGlobalCodeRef("CodeOwner::pinned"); + RuntimeCode code = code(); + slot.set(new RuntimeScalar(code)); + GlobalVariable.globalCodeRefs.remove("CodeOwner::pinned"); + assertEquals(1, code.refCount); + assertEquals(RuntimeBase.PerlLifecycleState.LIVE, code.perlLifecycleState()); + assertEquals(1, code.ownerSlotCount(PerlOwnerSlot.Kind.CODE_SLOT)); + GlobalVariable.clearPinnedCodeRefsForNamespace("CodeOwner::"); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, code.perlLifecycleState()); + } + } + + @Test + void firstAssignmentToAnEmptySlotAcquiresExactlyOneEdge() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + RuntimeScalar slot = new RuntimeScalar(); + GlobalVariable.globalCodeRefs.put("CodeOwner::empty", slot); + RuntimeCode first = code(); + slot.set(new RuntimeScalar(first)); + assertEquals(1, first.refCount); + long identity = slot.countedOwnerSlot().identity(); + RuntimeCode second = code(); + slot.set(new RuntimeScalar(second)); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, first.perlLifecycleState()); + assertEquals(1, second.refCount); + assertEquals(identity, slot.countedOwnerSlot().identity()); + slot.set(RuntimeScalarCache.scalarUndef); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, second.perlLifecycleState()); + GlobalVariable.globalCodeRefs.remove("CodeOwner::empty"); + } + } + + @Test + void wholeGlobNamesShareOnePhysicalCodeOwner() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + RuntimeCode code = code(); + RuntimeScalar slot = new RuntimeScalar(code); + GlobalVariable.globalCodeRefs.put("CodeOwner::first", slot); + GlobalVariable.globalCodeRefs.put("CodeOwner::second", slot); + assertEquals(1, code.refCount); + assertEquals(1, code.ownerSlotCount(PerlOwnerSlot.Kind.CODE_SLOT)); + long identity = slot.countedOwnerSlot().identity(); + GlobalVariable.globalCodeRefs.remove("CodeOwner::first"); + assertEquals(1, code.refCount); + assertEquals(identity, slot.countedOwnerSlot().identity()); + RuntimeScalar.scopeExitCleanup(slot); // A borrow cannot retire a GP slot. + assertEquals(1, code.refCount); + try (MortalList.ReachabilityQueryMeasurement measurement = MortalList.measureReachabilityQueries()) { + GlobalVariable.globalCodeRefs.remove("CodeOwner::second"); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, code.perlLifecycleState()); + assertEquals(0, code.ownerSlotCount(PerlOwnerSlot.Kind.CODE_SLOT)); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } + } + + @Test + void separateCodeSlotsOwnDistinctEdgesToTheSameCv() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + RuntimeCode code = code(); + RuntimeScalar first = new RuntimeScalar(code); + RuntimeScalar second = new RuntimeScalar(code); + GlobalVariable.globalCodeRefs.put("CodeOwner::first", first); + GlobalVariable.globalCodeRefs.put("CodeOwner::second", second); + assertEquals(2, code.refCount); + assertEquals(2, code.ownerSlotCount(PerlOwnerSlot.Kind.CODE_SLOT)); + assertNotEquals(first.countedOwnerSlot().identity(), second.countedOwnerSlot().identity()); + GlobalVariable.globalCodeRefs.remove("CodeOwner::first"); + assertEquals(1, code.refCount); + assertEquals(RuntimeBase.PerlLifecycleState.LIVE, code.perlLifecycleState()); + GlobalVariable.globalCodeRefs.entrySet().removeIf(e -> e.getKey().equals("CodeOwner::second")); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, code.perlLifecycleState()); + } + } + + @Test + void localizedSavedSlotKeepsItsOwnerUntilItIsRestored() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + RuntimeCode original = code(); + RuntimeCode temporary = code(); + RuntimeScalar saved = new RuntimeScalar(original); + GlobalVariable.globalCodeRefs.put("CodeOwner::localized", saved); + long identity = saved.countedOwnerSlot().identity(); + GlobalVariable.enterLocalizedCodeRef("CodeOwner::localized", saved); + GlobalVariable.globalCodeRefs.put("CodeOwner::localized", new RuntimeScalar(temporary)); + assertEquals(1, original.refCount); + assertEquals(identity, saved.countedOwnerSlot().identity()); + GlobalVariable.globalCodeRefs.put("CodeOwner::localized", saved); + GlobalVariable.exitLocalizedCodeRef("CodeOwner::localized", saved); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, temporary.perlLifecycleState()); + assertEquals(1, original.refCount); + assertEquals(identity, saved.countedOwnerSlot().identity()); + GlobalVariable.globalCodeRefs.clear(); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, original.perlLifecycleState()); + } + } + + @Test + void finalNamedOwnerWaitsForTheLastActiveCall() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + RuntimeCode code = code(); + GlobalVariable.globalCodeRefs.put("CodeOwner::active", new RuntimeScalar(code)); + RuntimeCode.pushActiveCode(code); + GlobalVariable.globalCodeRefs.remove("CodeOwner::active"); + assertEquals(0, code.refCount); + assertEquals(RuntimeBase.PerlLifecycleState.LIVE, code.perlLifecycleState()); + try (MortalList.ReachabilityQueryMeasurement measurement = MortalList.measureReachabilityQueries()) { + RuntimeCode.popActiveCode(code); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, code.perlLifecycleState()); + assertEquals(0, code.ownerSlotCount(PerlOwnerSlot.Kind.ACTIVE_CODE)); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/CorrectiveSweepDefaultPolicyTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/CorrectiveSweepDefaultPolicyTest.java new file mode 100644 index 0000000000..ca948d7300 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/CorrectiveSweepDefaultPolicyTest.java @@ -0,0 +1,46 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assumptions; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class CorrectiveSweepDefaultPolicyTest { + @Test + void defaultRuntimeDoesNotQueueOrExecuteCorrectiveSweeps() { + Assumptions.assumeFalse(ReachabilityWalker.correctiveSweepsEnabled(), + "legacy sweep comparison was explicitly enabled"); + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash referent = new RuntimeHash(); + referent.blessId = NameNormalizer.getBlessId("Issue1649::SweepPolicy"); + RuntimeScalar owner = new RuntimeScalar(); + owner.set(referent.createAnonymousReference()); + RuntimeScalar weak = new RuntimeScalar(); + weak.set(owner); + WeakRefRegistry.weaken(weak); + try (MortalList.ReachabilityQueryMeasurement measurement = MortalList.measureReachabilityQueries()) { + owner.scopeExited = true; + MortalList.noteDeferredScopeExitCaptureCandidate(owner); + MortalList.addDeferredCapture(owner); + assertTrue(runtime.lifecycleState.deferredScopeExitCaptureCandidates.isEmpty()); + MortalList.flushDeferredCapturesBeforeEnd(); + assertEquals(1, runtime.lifecycleState.deferredCaptures.size()); + MortalList.requestImmediateWeakSweep(); + MortalList.requestTargetedWeakSweep(referent); + assertFalse(MortalList.requestWeakSweepsForDestroyedContainer(referent)); + DestroyDispatch.requestSweepAfterOuterDestroy(); + assertFalse(runtime.lifecycleState.immediateWeakSweepRequested); + assertTrue(runtime.lifecycleState.targetedWeakSweepReferents.isEmpty()); + assertFalse(runtime.lifecycleState.sweepPendingAfterOuterDestroy); + assertEquals(0, ReachabilityWalker.sweepWeakRefs()); + assertEquals(0, ReachabilityWalker.sweepWeakRefs(true)); + assertEquals(RuntimeScalarType.HASHREFERENCE, weak.type); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/CountedOwnerSlotRepairsUnblessedReferentTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/CountedOwnerSlotRepairsUnblessedReferentTest.java new file mode 100644 index 0000000000..2566a5fdbf --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/CountedOwnerSlotRepairsUnblessedReferentTest.java @@ -0,0 +1,51 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class CountedOwnerSlotRepairsUnblessedReferentTest { + @Test + void scalarOverwriteUsesSurvivingCountedSlotWithoutRootWalk() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash target = new RuntimeHash(); + RuntimeArray arrayOwner = new RuntimeArray(); + arrayOwner.push(target.createAnonymousReference()); + RuntimeScalar scalarOwner = new RuntimeScalar(); + scalarOwner.set(target.createAnonymousReference()); + RuntimeScalar observer = target.createAnonymousReference(); + WeakRefRegistry.weaken(observer); + + try { + assertEquals(2, target.countedOwnerSlotCount()); + target.refCount = 1; // Model a transient legacy-count undercount. + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + scalarOwner.set(new RuntimeScalar()); + + assertEquals(1, target.refCount); + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.ARRAY_ELEMENT)); + assertTrue(WeakRefRegistry.isweak(observer)); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + arrayOwner.elements.getFirst().set(new RuntimeScalar()); + assertFalse(WeakRefRegistry.isweak(observer)); + } finally { + if (WeakRefRegistry.isweak(observer)) WeakRefRegistry.unweaken(observer); + scalarOwner.set(new RuntimeScalar()); + if (!arrayOwner.elements.isEmpty()) { + arrayOwner.elements.getFirst().set(new RuntimeScalar()); + } + } + } + } + +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/DeferredReleaseOwnerSlotFastPathTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/DeferredReleaseOwnerSlotFastPathTest.java new file mode 100644 index 0000000000..28350b79d3 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/DeferredReleaseOwnerSlotFastPathTest.java @@ -0,0 +1,49 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class DeferredReleaseOwnerSlotFastPathTest { + @Test + void remainingScalarOwnerRepairsDeferredZeroWithoutRootWalk() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash target = new RuntimeHash(); + target.blessId = NameNormalizer.getBlessId("Issue1649::DeferredOwnerSlot"); + RuntimeScalar released = new RuntimeScalar(); + released.set(target.createAnonymousReference()); + RuntimeScalar remaining = new RuntimeScalar(); + remaining.set(target.createAnonymousReference()); + RuntimeScalar weak = target.createAnonymousReference(); + WeakRefRegistry.weaken(weak); + + try { + assertEquals(2, target.countedOwnerSlotCount()); + target.refCount = 1; // Model a transient legacy-count underflow. + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + MortalList.deferDecrementIfTracked(released); + MortalList.flushAboveMark(); + + assertEquals(1, target.refCount); + assertEquals(1, target.countedOwnerSlotCount()); + assertTrue(WeakRefRegistry.isweak(weak)); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + + remaining.set(new RuntimeScalar()); + assertEquals(RuntimeScalarType.UNDEF, weak.type); + assertEquals(0, target.countedOwnerSlotCount()); + } + } finally { + if (WeakRefRegistry.isweak(weak)) WeakRefRegistry.unweaken(weak); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/DeferredUnblessedAggregateOwnerSlotRepairTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/DeferredUnblessedAggregateOwnerSlotRepairTest.java new file mode 100644 index 0000000000..2a25097485 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/DeferredUnblessedAggregateOwnerSlotRepairTest.java @@ -0,0 +1,48 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class DeferredUnblessedAggregateOwnerSlotRepairTest { + @Test + void deferredReleaseUsesCurrentArrayElementSlotWithoutRootSnapshot() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash target = new RuntimeHash(); + RuntimeArray ownerArray = new RuntimeArray(); + ownerArray.push(target.createAnonymousReference()); + RuntimeScalar weakObserver = target.createAnonymousReference(); + WeakRefRegistry.weaken(weakObserver); + + try { + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.ARRAY_ELEMENT)); + target.refCount = 1; // Model a transient legacy-count undercount. + MortalList.pushMark(); + MortalList.deferDecrement(target); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + MortalList.popAndFlush(); + + assertEquals(1, target.refCount); + assertTrue(WeakRefRegistry.isweak(weakObserver)); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + ownerArray.elements.getFirst().set(new RuntimeScalar()); + assertFalse(WeakRefRegistry.isweak(weakObserver)); + } finally { + if (WeakRefRegistry.isweak(weakObserver)) WeakRefRegistry.unweaken(weakObserver); + if (!ownerArray.elements.isEmpty()) { + ownerArray.elements.getFirst().set(new RuntimeScalar()); + } + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/EscapedScalarCellFinalOwnerReleaseTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/EscapedScalarCellFinalOwnerReleaseTest.java new file mode 100644 index 0000000000..f4697febd3 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/EscapedScalarCellFinalOwnerReleaseTest.java @@ -0,0 +1,69 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class EscapedScalarCellFinalOwnerReleaseTest { + @Test + void deferredCellCleanupRunsWhenItsLastOwnerLeaves() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + String className = "EscapedScalarCellFinalOwnerReleaseTest::Target"; + String destroyFqn = className + "::DESTROY"; + RuntimeHash target = new RuntimeHash(); + target.blessId = NameNormalizer.getBlessId(className); + target.refCount = 1; + + AtomicInteger destroyCalls = new AtomicInteger(); + RuntimeCode destroy = new RuntimeCode((args, context) -> { + destroyCalls.incrementAndGet(); + return new RuntimeList(); + }, null); + destroy.packageName = "EscapedScalarCellFinalOwnerReleaseTest"; + destroy.subName = "DESTROY"; + GlobalVariable.getGlobalCodeRef(destroyFqn).set(new RuntimeScalar(destroy)); + DestroyDispatch.registerIfDestroyable(target, target.blessId); + + RuntimeScalar cell = new RuntimeScalar(); + cell.type = RuntimeScalarType.HASHREFERENCE; + cell.value = target; + cell.refCountOwned = true; + target.activateOwnerTracking(); + target.recordActiveOwner(cell); + + RuntimeScalar weak = new RuntimeScalar(); + weak.type = RuntimeScalarType.HASHREFERENCE; + weak.value = target; + WeakRefRegistry.weaken(weak); + + // The wrapper's DESTROY cleanup sees its temporary self owner and + // must keep the escaped scalar cell's current value until that + // owner is released. + cell.referencedByScalarReference = true; + PerlOwnerSlot scalarCellOwner = new PerlOwnerSlot(PerlOwnerSlot.Kind.SCALAR_CELL); + scalarCellOwner.acquireNativeOwner(cell); + try { + RuntimeScalar.scopeExitCleanup(cell); + assertEquals(0, destroyCalls.get()); + assertTrue(WeakRefRegistry.isweak(weak), + "the observer remains weak while the scalar cell is owned"); + + scalarCellOwner.release(); + MortalList.flush(); + + assertEquals(1, destroyCalls.get(), "the nested object is destroyed once"); + assertEquals(RuntimeScalarType.UNDEF, weak.type, + "the nested object's weak observer clears with the final cell owner"); + } finally { + if (scalarCellOwner.isActive()) scalarCellOwner.release(); + GlobalVariable.globalCodeRefs.remove(destroyFqn); + DestroyDispatch.unregisterDestroyable(target); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/FinalCodeOwnerCaptureReleaseFastPathTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/FinalCodeOwnerCaptureReleaseFastPathTest.java new file mode 100644 index 0000000000..4e54d43828 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/FinalCodeOwnerCaptureReleaseFastPathTest.java @@ -0,0 +1,41 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +@Tag("unit") +class FinalCodeOwnerCaptureReleaseFastPathTest { + @Test + void finalScalarOwnerReleasesCapturesWithoutRootWalk() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeCode closure = new RuntimeCode((args, context) -> new RuntimeList(), null); + closure.refCount = 0; + RuntimeScalar capturedPad = new RuntimeScalar(); + PerlOwnerSlot captureSlot = capturedPad.retainClosureCaptureSlot(closure); + closure.capturedScalars = new RuntimeScalar[]{capturedPad}; + closure.capturedScalarOwnerSlots = new PerlOwnerSlot[]{captureSlot}; + + RuntimeScalar codeOwner = new RuntimeScalar(); + codeOwner.set(new RuntimeScalar(closure)); + assertEquals(1, closure.refCount); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + codeOwner.set(new RuntimeScalar()); + + assertNull(closure.capturedScalars, + "the final scalar owner releases the closure's captured pads"); + assertNull(closure.capturedScalarOwnerSlots); + assertEquals(0, capturedPad.captureCount); + assertFalse(captureSlot.isActive()); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/GrowingLexicalOutputOwnerSlotCostTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/GrowingLexicalOutputOwnerSlotCostTest.java new file mode 100644 index 0000000000..d870044ee0 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/GrowingLexicalOutputOwnerSlotCostTest.java @@ -0,0 +1,82 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class GrowingLexicalOutputOwnerSlotCostTest { + @Test + void nestedOutputCleanupDoesNotRescanTheGrowingLexicalRoot() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + final int resultCount = 256; + RuntimeArray output = new RuntimeArray(); + RuntimeArray[] records = new RuntimeArray[resultCount]; + RuntimeScalar[] weakLeaves = new RuntimeScalar[resultCount]; + MyVarCleanupStack.register(output); + + try { + for (int i = 0; i < resultCount; i++) { + RuntimeArray leaf = new RuntimeArray(); + RuntimeScalar leafOwner = leaf.createAnonymousReference(); + RuntimeScalar weakLeaf = leaf.createAnonymousReference(); + leaf.refCount = -1; + + RuntimeArray record = new RuntimeArray(); + record.elements.add(leafOwner); + RuntimeScalar recordOwner = record.createAnonymousReference(); + record.refCount = -1; + output.elements.add(recordOwner); + + WeakRefRegistry.weaken(weakLeaf); + records[i] = record; + weakLeaves[i] = weakLeaf; + } + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + for (RuntimeArray record : records) { + // Model a recursive callback frame ending after its + // result has been appended to the live output array. + MortalList.scopeExitCleanupArray(record); + } + + ReachabilityQueryStats stats = measurement.stats(); + assertEquals(resultCount, output.size()); + assertEquals(0, stats.rootQueries, + "explicit nested owner slots should replace per-result root proofs"); + assertEquals(0, stats.liveRootSnapshotsBuilt, + "a growing lexical output must not rebuild live-root snapshots"); + assertEquals(0, stats.externalRootSnapshotsBuilt); + assertEquals(0, stats.snapshotsBuilt); + assertEquals(0, stats.edgesInspected, + "no output-root edges should be revisited during local cleanup"); + for (RuntimeScalar weakLeaf : weakLeaves) { + assertTrue(WeakRefRegistry.isweak(weakLeaf), + "a live output row keeps its nested aggregate alive"); + } + } + + for (int i = resultCount - 1; i >= 0; i--) { + RuntimeScalar rootOwner = output.elements.removeLast(); + rootOwner.undefine(); + MortalList.scopeExitCleanupArray(records[i]); + MortalList.flush(); + assertFalse(WeakRefRegistry.isweak(weakLeaves[i]), + "the nested weak observer clears after its final output owner leaves"); + } + } finally { + for (RuntimeScalar weakLeaf : weakLeaves) { + if (weakLeaf != null) WeakRefRegistry.unweaken(weakLeaf); + } + MyVarCleanupStack.unregister(output); + MortalList.scopeExitCleanupArray(output); + MortalList.invalidateAllRootSnapshots(); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/LiveAggregateValueOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/LiveAggregateValueOwnerSlotTest.java new file mode 100644 index 0000000000..e52caf08a1 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/LiveAggregateValueOwnerSlotTest.java @@ -0,0 +1,62 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class LiveAggregateValueOwnerSlotTest { + @Test + void borrowedCellInLiveAggregateRetainsTrackedReferentUntilSlotLeaves() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + int lexicalMark = MyVarCleanupStack.pushMark(); + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar countedOwner = new RuntimeScalar(); + countedOwner.set(referent.createAnonymousReference()); + + RuntimeArray ownerContainer = new RuntimeArray(); + MyVarCleanupStack.register(ownerContainer); + RuntimeScalar uncountedOwner = referent.createAnonymousReference(); + ownerContainer.elements.add(uncountedOwner); + PerlOwnerSlot valueOwner = uncountedOwner.untrackedReferenceOwnerSlot(); + + RuntimeScalar weakObserver = referent.createAnonymousReference(); + WeakRefRegistry.weaken(weakObserver); + try { + assertNotNull(valueOwner, + "the live aggregate cell owns its uncounted value edge"); + assertEquals(1, + referent.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + countedOwner.set(new RuntimeScalar()); + + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, referent.refCount); + assertTrue(WeakRefRegistry.isweak(weakObserver), + "the aggregate cell keeps the weakly observed referent alive"); + + uncountedOwner.set(new RuntimeScalar()); + + assertFalse(valueOwner.isActive(), "the value owner leaves exactly once"); + assertEquals(0, + referent.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertFalse(WeakRefRegistry.isweak(weakObserver), + "the final owner release clears weak observers"); + assertEquals(RuntimeScalarType.UNDEF, weakObserver.type); + assertEquals(Integer.MIN_VALUE, referent.refCount); + } finally { + if (valueOwner != null && valueOwner.isActive()) { + uncountedOwner.set(new RuntimeScalar()); + } + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + countedOwner.set(new RuntimeScalar()); + MyVarCleanupStack.popMark(lexicalMark); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/LocalizedGlobCodeOwnerReleaseTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/LocalizedGlobCodeOwnerReleaseTest.java new file mode 100644 index 0000000000..af8368142a --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/LocalizedGlobCodeOwnerReleaseTest.java @@ -0,0 +1,63 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@Tag("unit") +class LocalizedGlobCodeOwnerReleaseTest { + @Test + void unownedLocalizedCodeReleasesCaptureWithoutReachabilityWalk() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + String name = "LocalizedGlobCodeOwnerReleaseTest::sub"; + RuntimeCode original = code(); + original.stashRefCount = 1; + GlobalVariable.globalCodeRefs.put(name, new RuntimeScalar(original)); + + RuntimeGlob glob = new RuntimeGlob(name); + RuntimeCode localized = code(); + localized.refCount = 2; + localized.stashRefCount = 1; + RuntimeScalar capturedPad = new RuntimeScalar(42); + localized.capturedScalars = new RuntimeScalar[] {capturedPad}; + localized.capturedScalarOwnerSlots = new PerlOwnerSlot[] { + capturedPad.retainClosureCaptureSlot(localized) + }; + localized.captureFieldsRecorded = true; + + glob.dynamicSaveState(); + RuntimeScalar localizedSlot = GlobalVariable.globalCodeRefs.get(name); + // Reproduce a legacy direct stash write that has no CODE-slot edge. + localizedSlot.type = RuntimeScalarType.CODE; + localizedSlot.value = localized; + localizedSlot.globalCodeRefFqn = name; + + try { + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + glob.dynamicRestoreState(); + + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, + localized.perlLifecycleState()); + assertNull(localized.capturedScalars); + assertEquals(0, capturedPad.captureCount); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().edgesInspected); + assertEquals(0, measurement.stats().nodesVisited); + } + } finally { + GlobalVariable.globalCodeRefs.remove(name); + if (localized.perlLifecycleState() != RuntimeBase.PerlLifecycleState.DESTROYED) { + localized.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroy(localized); + } + } + } + } + + private static RuntimeCode code() { + return new RuntimeCode((args, context) -> new RuntimeList(), null); + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/LocalizedGlobCodeOwnerSlotFastPathTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/LocalizedGlobCodeOwnerSlotFastPathTest.java new file mode 100644 index 0000000000..1208455eda --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/LocalizedGlobCodeOwnerSlotFastPathTest.java @@ -0,0 +1,63 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Tag("unit") +class LocalizedGlobCodeOwnerSlotFastPathTest { + @Test + void activeCallOwnerSkipsCaptureReachabilityWalkOnGlobRestore() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + String name = "LocalizedGlobCodeOwnerSlotFastPath::sub"; + RuntimeCode original = code(); + original.stashRefCount = 1; + GlobalVariable.globalCodeRefs.put(name, new RuntimeScalar(original)); + + RuntimeGlob glob = new RuntimeGlob(name); + RuntimeCode localized = code(); + localized.refCount = 2; + localized.stashRefCount = 1; + localized.capturedScalars = new RuntimeScalar[] {new RuntimeScalar(42)}; + + glob.dynamicSaveState(); + // Exercise the legacy stash path: the localized slot already has + // a CODE-slot binding while still undef. Bypass set() just as old + // direct stash writes did, so the CV has not opted into counted + // CODE-slot ownership yet. + RuntimeScalar localizedSlot = GlobalVariable.globalCodeRefs.get(name); + localizedSlot.type = RuntimeScalarType.CODE; + localizedSlot.value = localized; + localizedSlot.globalCodeRefFqn = name; + RuntimeCode.pushActiveCode(localized); + try { + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + glob.dynamicRestoreState(); + + assertEquals(0, localized.activeOwnerCount()); + assertEquals(1, localized.ownerSlotCount(PerlOwnerSlot.Kind.ACTIVE_CODE)); + assertEquals(1, localized.refCount, + "restoring the glob retires only the localized stash owner"); + assertNotNull(localized.capturedScalars, + "the active call keeps its captured pad alive"); + assertEquals(0, measurement.stats().rootQueries, + "the active-code owner slot replaces all three root checks"); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } finally { + RuntimeCode.popActiveCode(localized); + GlobalVariable.globalCodeRefs.remove(name); + localized.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroy(localized); + } + } + } + + private static RuntimeCode code() { + return new RuntimeCode((args, context) -> new RuntimeList(), null); + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/MethodInvocantOwnerTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/MethodInvocantOwnerTest.java new file mode 100644 index 0000000000..2cd01874d7 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/MethodInvocantOwnerTest.java @@ -0,0 +1,94 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class MethodInvocantOwnerTest { + private static RuntimeHash invocant() { + RuntimeHash value = new RuntimeHash(); + value.refCount = 0; + value.blessId = NameNormalizer.getBlessId("Issue1649::MethodOwner"); + return value; + } + + @Test + void eachCallAcquiresItsOwnArgumentIdentityAndRetiresItOnce() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash value = invocant(); + RuntimeScalar source = new RuntimeScalar(); + source.set(value.createAnonymousReference()); + RuntimeBase first = RuntimeCode.acquireMethodInvocantHold(source); + RuntimeBase second = RuntimeCode.acquireMethodInvocantHold(source); + assertNotSame(first, second); + assertEquals(2, value.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + assertEquals(2, value.ownerSlotSnapshot().stream() + .filter(slot -> slot.kind() == PerlOwnerSlot.Kind.ARGUMENT_VALUE) + .map(PerlOwnerSlot::identity).distinct().count()); + assertEquals(3, value.refCount); + RuntimeCode.releaseMethodInvocantHold(first); + RuntimeCode.releaseMethodInvocantHold(first); + assertEquals(2, value.refCount); + RuntimeCode.releaseAbandonedMethodInvocantHold(second); + RuntimeCode.releaseAbandonedMethodInvocantHold(second); + assertEquals(1, value.refCount); + assertEquals(0, value.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + assertEquals(0, runtime.positiveOwnerSlotReferentCount(value)); + } + } + + @Test + void finalNormalHoldCarriesItsIdentityUntilTheReceivingBoundaryDrains() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash value = invocant(); + RuntimeScalar source = new RuntimeScalar(); + source.set(value.createAnonymousReference()); + RuntimeScalar weak = new RuntimeScalar(); + weak.set(source); + WeakRefRegistry.weaken(weak); + RuntimeBase hold = RuntimeCode.acquireMethodInvocantHold(source); + source.set(RuntimeScalarCache.scalarUndef); + try (MortalList.ReachabilityQueryMeasurement measurement = MortalList.measureReachabilityQueries()) { + RuntimeCode.releaseMethodInvocantHold(hold); + RuntimeCode.releaseMethodInvocantHold(hold); + assertEquals(RuntimeScalarType.HASHREFERENCE, weak.type); + assertEquals(0, value.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + assertEquals(1, runtime.positiveOwnerSlotReferentCount(value)); + MortalList.flush(); + assertEquals(RuntimeScalarType.UNDEF, weak.type); + assertEquals(0, runtime.positiveOwnerSlotReferentCount(value)); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } + } + + @Test + void abandonedFinalHoldReleasesImmediatelyWithoutAReachabilityReplacement() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash value = invocant(); + RuntimeScalar source = new RuntimeScalar(); + source.set(value.createAnonymousReference()); + RuntimeScalar weak = new RuntimeScalar(); + weak.set(source); + WeakRefRegistry.weaken(weak); + RuntimeBase hold = RuntimeCode.acquireMethodInvocantHold(source); + source.set(RuntimeScalarCache.scalarUndef); + try (MortalList.ReachabilityQueryMeasurement measurement = MortalList.measureReachabilityQueries()) { + RuntimeCode.releaseAbandonedMethodInvocantHold(hold); + RuntimeCode.releaseAbandonedMethodInvocantHold(hold); + assertEquals(RuntimeScalarType.UNDEF, weak.type); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, value.perlLifecycleState()); + assertFalse(value.hasOwnerSlots()); + assertEquals(0, runtime.positiveOwnerSlotReferentCount(value)); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/MortalReturnUntrackedOwnerTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/MortalReturnUntrackedOwnerTest.java new file mode 100644 index 0000000000..214de2ce53 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/MortalReturnUntrackedOwnerTest.java @@ -0,0 +1,107 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class MortalReturnUntrackedOwnerTest { + @Test + void untrackedReturnOwnerLivesThroughNestedMortalBoundaryAndReleasesOnce() { + Assumptions.assumeFalse(ReachabilityWalker.correctiveSweepsEnabled(), + "return-slot ownership must release without corrective sweeps"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar target = new RuntimeScalar("return target"); + RuntimeScalar source = referenceTo(target); + + MortalList.pushMark(); + RuntimeScalar returned = RuntimeScalar.copyToMortalReturnSlot(source); + PerlOwnerSlot returnOwner = returned.untrackedReferenceOwnerSlot(); + assertNotNull(returnOwner); + assertSame(target, returnOwner.referent()); + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertEquals(-1, target.refCount, + "return ownership must not promote an untracked referent"); + + MortalList.pushMark(); + MortalList.popAndFlush(); + assertTrue(returnOwner.isActive(), + "an inner frame must not release its caller's return value"); + + MortalList.popAndFlush(); + assertFalse(returnOwner.isActive()); + assertEquals(0, target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertEquals(-1, target.refCount, + "releasing a return identity must leave the legacy count unchanged"); + } + } + + @Test + void weaklyTrackedReturnOwnerClearsObserverAfterLastScalarCellLeaves() { + Assumptions.assumeFalse(ReachabilityWalker.correctiveSweepsEnabled(), + "return-slot ownership must release without corrective sweeps"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar target = new RuntimeScalar("weakly tracked return target"); + RuntimeScalar source = new RuntimeScalar(); + RuntimeScalar weakObserver = referenceTo(target); + int lexicalMark = MyVarCleanupStack.pushMark(); + MyVarCleanupStack.register(source); + source.set(referenceTo(target)); + WeakRefRegistry.weaken(weakObserver); + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, target.refCount); + + try { + MortalList.pushMark(); + RuntimeScalar returned = RuntimeScalar.copyToMortalReturnSlot(source); + PerlOwnerSlot returnOwner = returned.untrackedReferenceOwnerSlot(); + assertNotNull(returnOwner); + assertEquals(2, + target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + MortalList.popAndFlush(); + assertFalse(returnOwner.isActive()); + assertEquals(1, + target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertTrue(WeakRefRegistry.isweak(weakObserver), + "the source lexical still owns the referent"); + + RuntimeScalar.scopeExitCleanup(source); + MyVarCleanupStack.unregister(source); + assertEquals(0, + target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertFalse(WeakRefRegistry.isweak(weakObserver)); + assertEquals(RuntimeScalarType.UNDEF, weakObserver.type); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + assertEquals(0, measurement.stats().scalarReachabilityQueries); + } + } finally { + RuntimeScalar.scopeExitCleanup(source); + MyVarCleanupStack.unregister(source); + MyVarCleanupStack.popMark(lexicalMark); + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + } + } + } + + private static RuntimeScalar referenceTo(RuntimeBase value) { + RuntimeScalar reference = new RuntimeScalar(); + reference.type = RuntimeScalarType.REFERENCE; + reference.value = value; + return reference; + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/MortalizeRegisteredCodeOwnerFastPathTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/MortalizeRegisteredCodeOwnerFastPathTest.java new file mode 100644 index 0000000000..70f2628e81 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/MortalizeRegisteredCodeOwnerFastPathTest.java @@ -0,0 +1,48 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class MortalizeRegisteredCodeOwnerFastPathTest { + @Test + void registeredContainerOwnerKeepsCodeWithoutScalarReachabilityQuery() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + MortalList.setActive(true); + int lexicalMark = MyVarCleanupStack.pushMark(); + RuntimeCode code = new RuntimeCode((args, context) -> new RuntimeList(), null); + String codeSlotName = "MortalizeRegisteredCodeOwnerFastPath::code"; + RuntimeScalar owner = new RuntimeScalar(code); + GlobalVariable.globalCodeRefs.put(codeSlotName, owner); + RuntimeArray registeredContainer = new RuntimeArray(); + registeredContainer.elements.add(owner); + MyVarCleanupStack.register(registeredContainer); + RuntimeList discardedResult = new RuntimeList(); + discardedResult.elements.add(owner); + + try { + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + MortalList.mortalizeForVoidDiscard(discardedResult); + + assertTrue(owner.refCountOwned, + "the registered container still owns its CODE scalar slot"); + assertEquals(1, code.refCount); + assertTrue(runtime.lifecycleState.pendingOwnerReleases.isEmpty()); + assertEquals(0, measurement.stats().scalarReachabilityQueries); + assertEquals(0, measurement.stats().scalarReachabilityRootWalks); + } + } finally { + registeredContainer.elements.remove(owner); + owner.releaseContainerOwner(registeredContainer); + MyVarCleanupStack.unregister(registeredContainer); + GlobalVariable.globalCodeRefs.remove(codeSlotName); + MyVarCleanupStack.popMark(lexicalMark); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/NestedUntrackedOwnerFinalCellCleanupTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/NestedUntrackedOwnerFinalCellCleanupTest.java new file mode 100644 index 0000000000..d2148206af --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/NestedUntrackedOwnerFinalCellCleanupTest.java @@ -0,0 +1,85 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class NestedUntrackedOwnerFinalCellCleanupTest { + @Test + void finalCellCleanupReleasesItsSlotWhileAnotherCountedOwnerKeepsReferentAlive() { + PerlRuntime runtime = new PerlRuntime(); + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar countedOwner = new RuntimeScalar(); + RuntimeScalar weakObserver = null; + RuntimeArray packageRoot = new RuntimeArray(); + String globalName = + "NestedUntrackedOwnerFinalCellCleanupTest::packageRoot"; + + try (PerlRuntime.Binding ignored = runtime.bind()) { + boolean previousMortalListState = MortalList.isActive(); + MortalList.setActive(true); + try { + countedOwner.set(referent.createAnonymousReference()); + weakObserver = referent.createAnonymousReference(); + WeakRefRegistry.weaken(weakObserver); + assertEquals(1, referent.refCount, + "the counted scalar is an independent live owner"); + + GlobalVariable.globalArrays.put(globalName, packageRoot); + RuntimeScalar nestedCell = referenceTo(referent); + packageRoot.elements.add(nestedCell); + nestedCell.markContainerOwner(packageRoot); + nestedCell.markPrepopulatedPackageAggregateOwner(packageRoot); + nestedCell.refreshUntrackedReferenceOwner(); + + PerlOwnerSlot nestedOwner = nestedCell.untrackedReferenceOwnerSlot(); + assertTrue(nestedOwner != null && nestedOwner.isActive()); + assertEquals(1, referent.ownerSlotCount( + PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + GlobalVariable.globalArrays.remove(globalName); + MortalList.scopeExitCleanupArray(packageRoot); + + assertFalse(nestedOwner.isActive(), + "final cell cleanup retires this exact nested edge"); + assertEquals(0, referent.ownerSlotCount( + PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertEquals(1, referent.refCount, + "releasing the nested edge must preserve the separate counted owner"); + assertEquals(1, referent.countedOwnerSlotCount()); + assertTrue(WeakRefRegistry.isweak(weakObserver), + "the remaining counted owner keeps the weak observer live"); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + countedOwner.undefine(); + assertFalse(WeakRefRegistry.isweak(weakObserver), + "the final counted owner clears the observer when it leaves"); + assertEquals(Integer.MIN_VALUE, referent.refCount); + } finally { + if (weakObserver != null && WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + GlobalVariable.globalArrays.remove(globalName); + MortalList.scopeExitCleanupArray(packageRoot); + countedOwner.undefine(); + MortalList.setActive(previousMortalListState); + MortalList.invalidateAllRootSnapshots(); + } + } + } + + private static RuntimeScalar referenceTo(RuntimeBase referent) { + RuntimeScalar scalar = new RuntimeScalar(); + scalar.type = RuntimeScalarType.HASHREFERENCE; + scalar.value = referent; + return scalar; + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/PackageAggregateOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/PackageAggregateOwnerSlotTest.java new file mode 100644 index 0000000000..2ccb81d4af --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/PackageAggregateOwnerSlotTest.java @@ -0,0 +1,208 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class PackageAggregateOwnerSlotTest { + @Test + void packageArrayAndHashKeysOwnDistinctRootIdentities() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray array = new RuntimeArray(); + RuntimeHash hash = new RuntimeHash(); + int arrayRefCount = array.refCount; + int hashRefCount = hash.refCount; + String firstArrayKey = "PackageAggregateOwnerSlotTest::array_one"; + String secondArrayKey = "PackageAggregateOwnerSlotTest::array_two"; + String sharedKey = "PackageAggregateOwnerSlotTest::shared_name"; + + try { + GlobalVariable.globalArrays.put(firstArrayKey, array); + PerlOwnerSlot firstArrayOwner = array.ownerSlotSnapshot().stream() + .filter(slot -> slot.kind() == PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT) + .findFirst().orElse(null); + assertNotNull(firstArrayOwner); + assertSame(array, firstArrayOwner.referent()); + assertEquals(1, array.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT)); + + GlobalVariable.globalArrays.put(secondArrayKey, array); + assertEquals(2, array.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT), + "each package key owns an independent array root slot"); + PerlOwnerSlot secondArrayOwner = array.ownerSlotSnapshot().stream() + .filter(slot -> slot.kind() == PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT + && slot != firstArrayOwner) + .findFirst().orElse(null); + assertNotNull(secondArrayOwner); + + GlobalVariable.globalHashes.put(sharedKey, hash); + assertEquals(1, hash.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT), + "array and hash roots with one name have separate identities"); + + GlobalVariable.globalArrays.keySet().remove(firstArrayKey); + assertFalse(firstArrayOwner.isActive()); + assertTrue(secondArrayOwner.isActive()); + assertEquals(1, array.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT)); + + GlobalVariable.globalArrays.remove(secondArrayKey); + GlobalVariable.globalHashes.remove(sharedKey); + assertFalse(secondArrayOwner.isActive()); + assertEquals(0, array.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT)); + assertEquals(0, hash.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT)); + assertEquals(arrayRefCount, array.refCount, + "package root identities must not promote the aggregate count"); + assertEquals(hashRefCount, hash.refCount, + "package root identities must not promote the aggregate count"); + + GlobalVariable.globalArrays.put(firstArrayKey, array); + GlobalVariable.globalHashes.put(sharedKey, hash); + PerlOwnerSlot clearArrayOwner = array.ownerSlotSnapshot().stream() + .filter(slot -> slot.isActive() + && slot.kind() == PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT) + .findFirst().orElseThrow(); + PerlOwnerSlot clearHashOwner = hash.ownerSlotSnapshot().stream() + .filter(slot -> slot.isActive() + && slot.kind() == PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT) + .findFirst().orElseThrow(); + GlobalVariable.globalArrays.clear(); + GlobalVariable.globalHashes.clear(); + assertFalse(clearArrayOwner.isActive()); + assertFalse(clearHashOwner.isActive()); + } finally { + GlobalVariable.globalArrays.remove(firstArrayKey); + GlobalVariable.globalArrays.remove(secondArrayKey); + GlobalVariable.globalHashes.remove(sharedKey); + } + } + } + + @Test + void packageArrayAndHashRootsRebuildFromSnapshotTables() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray array = new RuntimeArray(); + RuntimeHash hash = new RuntimeHash(); + runtime.globalState.arrayValues().put("PackageAggregateOwnerSlotTest::clone_array", array); + runtime.globalState.hashValues().put("PackageAggregateOwnerSlotTest::clone_hash", hash); + + runtime.globalState.rebuildPackageAggregateOwnerSlots(); + + assertEquals(1, array.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT)); + assertEquals(1, hash.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT)); + assertEquals(-1, array.refCount); + assertEquals(-1, hash.refCount); + } + } + + @Test + void packageRootEntryValueReplacementTransfersItsIdentity() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray first = new RuntimeArray(); + RuntimeArray replacement = new RuntimeArray(); + String key = "PackageAggregateOwnerSlotTest::replace_array"; + + try { + GlobalVariable.globalArrays.put(key, first); + PerlOwnerSlot owner = first.ownerSlotSnapshot().stream() + .filter(slot -> slot.kind() == PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT) + .findFirst().orElseThrow(); + GlobalVariable.globalArrays.entrySet().stream() + .filter(entry -> entry.getKey().equals(key)) + .findFirst().orElseThrow().setValue(replacement); + + assertSame(replacement, owner.referent()); + assertEquals(0, first.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT)); + assertEquals(1, + replacement.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT)); + + GlobalVariable.globalArrays.remove(key); + assertFalse(owner.isActive()); + assertEquals(0, + replacement.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT)); + } finally { + GlobalVariable.globalArrays.remove(key); + } + } + } + + @Test + void finalPackageAggregateRootClearsWeakObserverWithoutSweeps() { + Assumptions.assumeFalse(ReachabilityWalker.correctiveSweepsEnabled(), + "package-root release must clear weak refs without corrective sweeps"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray array = new RuntimeArray(); + RuntimeScalar weakObserver = referenceTo(array); + WeakRefRegistry.weaken(weakObserver); + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, array.refCount); + String key = "PackageAggregateOwnerSlotTest::weak_array"; + + try { + GlobalVariable.globalArrays.put(key, array); + assertEquals(1, array.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT)); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + GlobalVariable.globalArrays.remove(key); + + assertEquals(0, array.ownerSlotCount( + PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT)); + assertFalse(WeakRefRegistry.isweak(weakObserver)); + assertEquals(RuntimeScalarType.UNDEF, weakObserver.type); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + assertEquals(0, measurement.stats().scalarReachabilityQueries); + } + } finally { + GlobalVariable.globalArrays.remove(key); + if (WeakRefRegistry.isweak(weakObserver)) WeakRefRegistry.unweaken(weakObserver); + } + } + } + + @Test + void finalPackageScalarRootClearsWeakObserverToCellWithoutSweeps() { + Assumptions.assumeFalse(ReachabilityWalker.correctiveSweepsEnabled(), + "package-root release must clear weak refs without corrective sweeps"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar packageCell = new RuntimeScalar("package scalar cell"); + RuntimeScalar weakObserver = referenceTo(packageCell); + WeakRefRegistry.weaken(weakObserver); + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, packageCell.refCount); + String key = "PackageAggregateOwnerSlotTest::weak_scalar"; + + try { + GlobalVariable.globalVariables.put(key, packageCell); + assertEquals(1, packageCell.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_SCALAR_SLOT)); + + GlobalVariable.globalVariables.remove(key); + + assertEquals(0, packageCell.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_SCALAR_SLOT)); + assertFalse(WeakRefRegistry.isweak(weakObserver)); + assertEquals(RuntimeScalarType.UNDEF, weakObserver.type); + } finally { + GlobalVariable.globalVariables.remove(key); + if (WeakRefRegistry.isweak(weakObserver)) WeakRefRegistry.unweaken(weakObserver); + } + } + } + + private static RuntimeScalar referenceTo(RuntimeBase value) { + RuntimeScalar reference = new RuntimeScalar(); + reference.type = RuntimeScalarType.REFERENCE; + reference.value = value; + return reference; + } + +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/PackageAggregateValueOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/PackageAggregateValueOwnerSlotTest.java new file mode 100644 index 0000000000..ed33369051 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/PackageAggregateValueOwnerSlotTest.java @@ -0,0 +1,244 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class PackageAggregateValueOwnerSlotTest { + @Test + void prepopulatedPackageArrayOwnsUncountedValueWithoutSweeps() { + Assumptions.assumeFalse(ReachabilityWalker.correctiveSweepsEnabled(), + "package aggregate value ownership must not depend on corrective sweeps"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar referent = new RuntimeScalar("package aggregate referent"); + RuntimeScalar countedOwner = new RuntimeScalar(); + countedOwner.set(referent.createReference()); + + RuntimeScalar aggregateValue = referenceTo(referent); + RuntimeArray packageArray = new RuntimeArray(); + packageArray.elements.add(aggregateValue); + String key = "PackageAggregateValueOwnerSlotTest::values"; + RuntimeScalar weakObserver = referenceTo(referent); + PerlOwnerSlot valueOwner = null; + + try { + GlobalVariable.globalArrays.put(key, packageArray); + valueOwner = aggregateValue.untrackedReferenceOwnerSlot(); + assertNotNull(valueOwner, + "rooting a prepopulated aggregate records its uncounted value edge"); + assertEquals(1, + referent.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + WeakRefRegistry.weaken(weakObserver); + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + countedOwner.set(new RuntimeScalar()); + assertTrue(WeakRefRegistry.isweak(weakObserver), + "the package aggregate value keeps the referent alive"); + + aggregateValue.set(new RuntimeScalar()); + + assertFalse(valueOwner.isActive(), "the value owner releases exactly once"); + assertEquals(0, + referent.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertFalse(WeakRefRegistry.isweak(weakObserver)); + assertEquals(RuntimeScalarType.UNDEF, weakObserver.type); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } finally { + if (valueOwner != null && valueOwner.isActive()) { + aggregateValue.set(new RuntimeScalar()); + } + GlobalVariable.globalArrays.remove(key); + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + countedOwner.set(new RuntimeScalar()); + } + } + } + + @Test + void prepopulatedPackageHashOwnsUncountedValueWithoutSweeps() { + Assumptions.assumeFalse(ReachabilityWalker.correctiveSweepsEnabled(), + "package aggregate value ownership must not depend on corrective sweeps"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar referent = new RuntimeScalar("package hash referent"); + RuntimeScalar countedOwner = new RuntimeScalar(); + countedOwner.set(referent.createReference()); + + RuntimeScalar aggregateValue = referenceTo(referent); + RuntimeHash packageHash = new RuntimeHash(); + packageHash.elements.put("child", aggregateValue); + String key = "PackageAggregateValueOwnerSlotTest::hash_values"; + RuntimeScalar weakObserver = referenceTo(referent); + PerlOwnerSlot valueOwner = null; + + try { + GlobalVariable.globalHashes.put(key, packageHash); + valueOwner = aggregateValue.untrackedReferenceOwnerSlot(); + assertNotNull(valueOwner, + "rooting a prepopulated hash records its uncounted value edge"); + assertEquals(1, + referent.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + WeakRefRegistry.weaken(weakObserver); + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + countedOwner.set(new RuntimeScalar()); + assertTrue(WeakRefRegistry.isweak(weakObserver), + "the package hash value keeps the referent alive"); + + aggregateValue.set(new RuntimeScalar()); + + assertFalse(valueOwner.isActive(), "the value owner releases exactly once"); + assertEquals(0, + referent.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertFalse(WeakRefRegistry.isweak(weakObserver)); + assertEquals(RuntimeScalarType.UNDEF, weakObserver.type); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } finally { + if (valueOwner != null && valueOwner.isActive()) { + aggregateValue.set(new RuntimeScalar()); + } + GlobalVariable.globalHashes.remove(key); + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + countedOwner.set(new RuntimeScalar()); + } + } + } + + @Test + void packageAggregateValuesOwnArrayAndHashReferentsWithoutSweeps() { + Assumptions.assumeFalse(ReachabilityWalker.correctiveSweepsEnabled(), + "package aggregate value ownership must not depend on corrective sweeps"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + assertPackageAggregateValueKeepsReferentAlive( + new RuntimeArray(), new RuntimeArray(), true, + "PackageAggregateValueOwnerSlotTest::array_referent"); + assertPackageAggregateValueKeepsReferentAlive( + new RuntimeHash(), new RuntimeHash(), false, + "PackageAggregateValueOwnerSlotTest::hash_referent"); + } + } + + @Test + void crossContainerAliasKeepsOneValueOwnerUntilItsFinalPackageSlotLeaves() { + Assumptions.assumeFalse(ReachabilityWalker.correctiveSweepsEnabled(), + "cross-container alias ownership must not depend on corrective sweeps"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray packageArray = new RuntimeArray(); + RuntimeHash packageHash = new RuntimeHash(); + RuntimeArray referent = new RuntimeArray(); + RuntimeScalar sharedValue = referenceTo(referent); + RuntimeScalar weakObserver = referenceTo(referent); + String arrayKey = "PackageAggregateValueOwnerSlotTest::alias_array"; + String hashKey = "PackageAggregateValueOwnerSlotTest::alias_hash"; + + try { + GlobalVariable.globalArrays.put(arrayKey, packageArray); + GlobalVariable.globalHashes.put(hashKey, packageHash); + packageArray.elements.add(sharedValue); + packageHash.elements.put("shared", sharedValue); + + PerlOwnerSlot owner = sharedValue.untrackedReferenceOwnerSlot(); + assertNotNull(owner); + assertEquals(1, + referent.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + WeakRefRegistry.weaken(weakObserver); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + packageArray.elements.remove(sharedValue); + + assertTrue(WeakRefRegistry.isweak(weakObserver), + "the hash alias remains a strong owner after the array slot leaves"); + assertTrue(owner.isActive()); + + packageHash.elements.remove("shared"); + + assertFalse(owner.isActive(), "the shared value owner releases once"); + assertFalse(WeakRefRegistry.isweak(weakObserver)); + assertEquals(RuntimeScalarType.UNDEF, weakObserver.type); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } finally { + GlobalVariable.globalArrays.remove(arrayKey); + GlobalVariable.globalHashes.remove(hashKey); + if (WeakRefRegistry.isweak(weakObserver)) WeakRefRegistry.unweaken(weakObserver); + } + } + } + + private static void assertPackageAggregateValueKeepsReferentAlive( + RuntimeBase referent, RuntimeBase packageAggregate, boolean packageArray, + String key) { + RuntimeScalar aggregateValue = referenceTo(referent); + if (packageArray) { + ((RuntimeArray) packageAggregate).elements.add(aggregateValue); + GlobalVariable.globalArrays.put(key, (RuntimeArray) packageAggregate); + } else { + ((RuntimeHash) packageAggregate).elements.put("child", aggregateValue); + GlobalVariable.globalHashes.put(key, (RuntimeHash) packageAggregate); + } + RuntimeScalar weakObserver = referenceTo(referent); + PerlOwnerSlot valueOwner = aggregateValue.untrackedReferenceOwnerSlot(); + + try { + assertNotNull(valueOwner, + "a package aggregate slot records its direct aggregate referent edge"); + assertEquals(1, + referent.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + WeakRefRegistry.weaken(weakObserver); + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + assertTrue(WeakRefRegistry.isweak(weakObserver)); + + aggregateValue.set(new RuntimeScalar()); + + assertFalse(valueOwner.isActive()); + assertEquals(0, + referent.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertFalse(WeakRefRegistry.isweak(weakObserver)); + assertEquals(RuntimeScalarType.UNDEF, weakObserver.type); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } finally { + if (valueOwner != null && valueOwner.isActive()) { + aggregateValue.set(new RuntimeScalar()); + } + if (packageArray) GlobalVariable.globalArrays.remove(key); + else GlobalVariable.globalHashes.remove(key); + if (WeakRefRegistry.isweak(weakObserver)) WeakRefRegistry.unweaken(weakObserver); + } + } + + private static RuntimeScalar referenceTo(RuntimeBase referent) { + RuntimeScalar reference = new RuntimeScalar(); + reference.type = RuntimeScalarType.REFERENCE; + reference.value = referent; + return reference; + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/PackageNestedUntrackedAggregateReleaseTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/PackageNestedUntrackedAggregateReleaseTest.java new file mode 100644 index 0000000000..d7f0878163 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/PackageNestedUntrackedAggregateReleaseTest.java @@ -0,0 +1,104 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class PackageNestedUntrackedAggregateReleaseTest { + @Test + void firstWeakRootCheckUsesOwnerSlotWithoutWalkingPackageRoots() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray packageRoot = new RuntimeArray(); + String globalName = + "PackageNestedUntrackedAggregateReleaseTest::ownerSlotFastPath"; + GlobalVariable.globalArrays.put(globalName, packageRoot); + + RuntimeArray target = new RuntimeArray(); + RuntimeScalar strong = target.createAnonymousReference(); + target.refCount = -1; + packageRoot.elements.add(strong); + RuntimeScalar weak = target.createAnonymousReference(); + target.refCount = -1; + + try { + assertEquals(1, + target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + WeakRefRegistry.weaken(weak); + + ReachabilityQueryStats stats = measurement.stats(); + assertTrue(WeakRefRegistry.isweak(weak)); + assertEquals(1, stats.rootQueries, + "the compatibility entry point still records its owner proof"); + assertEquals(0, stats.edgesInspected, + "an active untracked owner slot avoids scanning package roots"); + assertEquals(0, stats.nodesVisited); + assertEquals(0, stats.rootsSeeded); + } + } finally { + WeakRefRegistry.clearWeakRefsTo(target); + GlobalVariable.globalArrays.remove(globalName); + MortalList.invalidateAllRootSnapshots(); + } + } + } + + @Test + void finalUntrackedPackageOwnerClearsWeakObserver() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray packageRoot = new RuntimeArray(); + String globalName = + "PackageNestedUntrackedAggregateReleaseTest::packageRoot"; + GlobalVariable.globalArrays.put(globalName, packageRoot); + + RuntimeArray target = new RuntimeArray(); + RuntimeScalar firstOwner = target.createAnonymousReference(); + target.refCount = -1; + packageRoot.elements.add(firstOwner); + RuntimeScalar secondOwner = target.createAnonymousReference(); + target.refCount = -1; + packageRoot.elements.add(secondOwner); + RuntimeScalar weak = target.createAnonymousReference(); + target.refCount = -1; + + try { + assertEquals(2, + target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + WeakRefRegistry.weaken(weak); + + assertTrue(WeakRefRegistry.isweak(weak)); + assertEquals(1, target.refCount, + "the package-root proof installs a temporary compatibility pin"); + assertTrue(target.packageRootRescuedUntrackedAggregate); + + firstOwner.undefine(); + assertTrue(WeakRefRegistry.isweak(weak), + "one remaining package owner keeps the weak observer live"); + assertEquals(1, + target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + secondOwner.undefine(); + + assertFalse(WeakRefRegistry.isweak(weak)); + assertNull(weak.value, + "the observer is cleared when the final untracked owner leaves"); + assertEquals(Integer.MIN_VALUE, target.refCount); + assertFalse(target.packageRootRescuedUntrackedAggregate); + } finally { + WeakRefRegistry.clearWeakRefsTo(target); + GlobalVariable.globalArrays.remove(globalName); + MortalList.invalidateAllRootSnapshots(); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/PackageRootOwnerSlotFastPathTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/PackageRootOwnerSlotFastPathTest.java new file mode 100644 index 0000000000..2cf7e966bb --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/PackageRootOwnerSlotFastPathTest.java @@ -0,0 +1,62 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class PackageRootOwnerSlotFastPathTest { + @Test + void survivingPackageHashSlotRepairsTransientZeroWithoutRootWalk() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + String leftName = "PackageRootOwnerSlotFastPath::left"; + String rightName = "PackageRootOwnerSlotFastPath::right"; + RuntimeHash left = new RuntimeHash(); + RuntimeHash right = new RuntimeHash(); + GlobalVariable.aliasGlobalHash(leftName, left); + GlobalVariable.aliasGlobalHash(rightName, right); + + RuntimeHash target = new RuntimeHash(); + target.blessId = NameNormalizer.getBlessId("PackageRootOwnerSlotFastPath::Target"); + left.put("target", target.createAnonymousReference()); + right.put("target", target.createAnonymousReference()); + RuntimeScalar observer = new RuntimeScalar(); + observer.set(target.createAnonymousReference()); + WeakRefRegistry.weaken(observer); + + try { + assertTrue(target.storedInPackageGlobal); + assertEquals(2, target.countedOwnerSlotCount()); + assertTrue(WeakRefRegistry.isweak(observer)); + + // Model the selective count's transient undercount. The two + // package hash slots remain the authoritative owner ledger. + target.refCount = 1; + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + left.elements.get("target").set(new RuntimeScalar()); + + assertEquals(1, target.refCount); + assertEquals(1, target.countedOwnerSlotCount()); + assertTrue(WeakRefRegistry.isweak(observer)); + assertFalse(target.destroyFired); + assertEquals(0, measurement.stats().rootQueries, + "the remaining counted slot repairs the transient zero"); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + right.elements.get("target").set(new RuntimeScalar()); + assertFalse(WeakRefRegistry.isweak(observer), + "the final counted owner still releases the weak observer"); + } finally { + if (WeakRefRegistry.isweak(observer)) WeakRefRegistry.unweaken(observer); + GlobalVariable.globalHashes.remove(leftName); + GlobalVariable.globalHashes.remove(rightName); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/PackageScalarValueOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/PackageScalarValueOwnerSlotTest.java new file mode 100644 index 0000000000..4ae4951e81 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/PackageScalarValueOwnerSlotTest.java @@ -0,0 +1,55 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +@Tag("unit") +class PackageScalarValueOwnerSlotTest { + @Test + void trackedReferentGetsOneUncountedIdentityForPackageScalarCell() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar countedOwner = new RuntimeScalar(); + countedOwner.set(referent.createAnonymousReference()); + + RuntimeScalar packageCell = referent.createAnonymousReference(); + String globalName = + "PackageScalarValueOwnerSlotTest::trackedPackageCell"; + PerlOwnerSlot valueOwner; + + try { + GlobalVariable.globalVariables.put(globalName, packageCell); + valueOwner = packageCell.untrackedReferenceOwnerSlot(); + + assertNotNull(valueOwner, + "the package scalar cell owns its uncounted value edge"); + assertSame(referent, valueOwner.referent()); + assertEquals(1, referent.refCount, + "the extra slot identity does not duplicate the legacy count"); + assertEquals(1, + referent.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + GlobalVariable.globalVariables.remove(globalName); + + assertFalse(valueOwner.isActive(), + "removing the package cell releases its value edge once"); + assertEquals(0, + referent.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertEquals(1, referent.refCount, + "the independent counted scalar owner remains"); + + countedOwner.set(new RuntimeScalar()); + assertEquals(Integer.MIN_VALUE, referent.refCount, + "the final counted owner performs final release"); + } finally { + GlobalVariable.globalVariables.remove(globalName); + countedOwner.set(new RuntimeScalar()); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/PackageWeakCompatibilityPinOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/PackageWeakCompatibilityPinOwnerSlotTest.java new file mode 100644 index 0000000000..0af5683971 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/PackageWeakCompatibilityPinOwnerSlotTest.java @@ -0,0 +1,110 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class PackageWeakCompatibilityPinOwnerSlotTest { + @Test + void packageWeakPinHasIdentityAndReleasesAfterTheFinalScalarOwner() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + RuntimeArray packageRoot = new RuntimeArray(); + String globalName = "PackageWeakCompatibilityPinOwnerSlotTest::packageRoot"; + GlobalVariable.globalArrays.put(globalName, packageRoot); + + RuntimeArray target = new RuntimeArray(); + RuntimeScalar firstOwner = target.createAnonymousReference(); + target.refCount = -1; + packageRoot.elements.add(firstOwner); + RuntimeScalar secondOwner = target.createAnonymousReference(); + target.refCount = -1; + packageRoot.elements.add(secondOwner); + RuntimeScalar weakObserver = target.createAnonymousReference(); + target.refCount = -1; + + try { + WeakRefRegistry.weaken(weakObserver); + + PerlOwnerSlot compatibilityPin = target.packageRootRescueOwnerSlot; + assertTrue(target.packageRootRescuedUntrackedAggregate); + assertTrue(compatibilityPin.isActive()); + assertEquals(1, target.ownerSlotCount( + PerlOwnerSlot.Kind.WEAKLY_TRACKED_COMPATIBILITY_PIN)); + assertEquals(2, target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + firstOwner.undefine(); + assertTrue(WeakRefRegistry.isweak(weakObserver)); + assertTrue(compatibilityPin.isActive(), + "the bridge remains while a scalar owner slot remains"); + + secondOwner.undefine(); + assertFalse(compatibilityPin.isActive(), + "the compatibility owner identity releases exactly once"); + assertFalse(target.packageRootRescuedUntrackedAggregate); + assertEquals(0, target.ownerSlotCount( + PerlOwnerSlot.Kind.WEAKLY_TRACKED_COMPATIBILITY_PIN)); + assertEquals(Integer.MIN_VALUE, target.refCount); + assertFalse(WeakRefRegistry.isweak(weakObserver)); + + compatibilityPin.release(); + assertEquals(Integer.MIN_VALUE, target.refCount, + "repeated slot release cannot revive or release the referent twice"); + } finally { + WeakRefRegistry.clearWeakRefsTo(target); + GlobalVariable.globalArrays.remove(globalName); + MortalList.invalidateAllRootSnapshots(); + } + } + } + + @Test + void unweakenedReferenceKeepsItsCountWhenPackageOwnersLeave() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + RuntimeArray packageRoot = new RuntimeArray(); + String globalName = "PackageWeakCompatibilityPinOwnerSlotTest::unweakenedRoot"; + GlobalVariable.globalArrays.put(globalName, packageRoot); + + RuntimeArray target = new RuntimeArray(); + RuntimeScalar firstOwner = target.createAnonymousReference(); + target.refCount = -1; + packageRoot.elements.add(firstOwner); + RuntimeScalar secondOwner = target.createAnonymousReference(); + target.refCount = -1; + packageRoot.elements.add(secondOwner); + RuntimeScalar observer = target.createAnonymousReference(); + target.refCount = -1; + + try { + WeakRefRegistry.weaken(observer); + PerlOwnerSlot compatibilityPin = target.packageRootRescueOwnerSlot; + WeakRefRegistry.unweaken(observer); + + assertEquals(2, target.refCount, + "the strong observer count includes its owner and the compatibility pin"); + assertTrue(compatibilityPin.isActive()); + + firstOwner.undefine(); + secondOwner.undefine(); + + assertFalse(compatibilityPin.isActive(), + "the final package slot retires only the compatibility count"); + assertEquals(1, target.refCount, + "the unweakened scalar's real owner count remains"); + assertEquals(0, target.ownerSlotCount( + PerlOwnerSlot.Kind.WEAKLY_TRACKED_COMPATIBILITY_PIN)); + + observer.undefine(); + assertEquals(Integer.MIN_VALUE, target.refCount, + "the unweakened scalar releases the remaining real count"); + } finally { + WeakRefRegistry.clearWeakRefsTo(target); + GlobalVariable.globalArrays.remove(globalName); + MortalList.invalidateAllRootSnapshots(); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/PendingOwnerReleaseFinalizationTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/PendingOwnerReleaseFinalizationTest.java new file mode 100644 index 0000000000..47ffe6d99d --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/PendingOwnerReleaseFinalizationTest.java @@ -0,0 +1,66 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class PendingOwnerReleaseFinalizationTest { + @Test + void clearedAggregateFinalizationWaitsForQueuedCountedRelease() { + PerlRuntime runtime = new PerlRuntime(); + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar owner = new RuntimeScalar(); + RuntimeScalar weakObserver = referenceTo(referent); + + try (PerlRuntime.Binding ignored = runtime.bind()) { + boolean previousMortalListState = MortalList.isActive(); + MortalList.setActive(true); + try { + owner.set(referent.createAnonymousReference()); + WeakRefRegistry.weaken(weakObserver); + assertEquals(1, referent.refCount); + assertEquals(1, referent.countedOwnerSlotCount()); + + referent.clearedOwnedAggregateElement = true; + MortalList.deferDecrementIfTracked(owner); + + assertFalse(owner.refCountOwned); + assertEquals(0, referent.countedOwnerSlotCount()); + assertEquals(1, referent.refCount, + "the queued release has not decremented the compatibility count yet"); + assertTrue(referent.hasPendingOwnerRelease()); + + MortalList.finalizeClearedAggregateOwnerAfterScopeExit(referent); + + assertTrue(WeakRefRegistry.isweak(weakObserver), + "scope cleanup must leave the queued release to decide finalization"); + assertEquals(1, referent.refCount); + + MortalList.flush(); + + assertFalse(WeakRefRegistry.isweak(weakObserver), + "draining the final counted release clears the weak observer"); + assertEquals(Integer.MIN_VALUE, referent.refCount); + assertFalse(referent.hasPendingOwnerRelease()); + } finally { + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + if (referent.hasPendingOwnerRelease()) MortalList.flush(); + MortalList.invalidateAllRootSnapshots(); + MortalList.setActive(previousMortalListState); + } + } + } + + private static RuntimeScalar referenceTo(RuntimeBase referent) { + RuntimeScalar scalar = new RuntimeScalar(); + scalar.type = RuntimeScalarType.HASHREFERENCE; + scalar.value = referent; + return scalar; + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/PerlArgumentUnwindOwnerTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/PerlArgumentUnwindOwnerTest.java new file mode 100644 index 0000000000..e2fa7368bb --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/PerlArgumentUnwindOwnerTest.java @@ -0,0 +1,148 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class PerlArgumentUnwindOwnerTest { + @Test + void argumentArrayReplacementReleasesInsertedOwnersButBorrowsCallerCells() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash callerReferent = new RuntimeHash(); + RuntimeScalar caller = new RuntimeScalar(); + caller.set(callerReferent.createAnonymousReference()); + PerlOwnerSlot callerSlot = caller.countedOwnerSlot(); + RuntimeArray arguments = new RuntimeArray(); + caller.setArrayOfAlias(arguments); + RuntimeHash inserted = new RuntimeHash(); + arguments.push(inserted.createAnonymousReference()); + assertEquals(1, inserted.refCount); + arguments.setFromList(new RuntimeList()); + assertTrue(callerSlot.isActive()); + assertTrue(caller.refCountOwned); + assertEquals(1, callerReferent.refCount); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, inserted.perlLifecycleState()); + caller.setArrayOfAlias(arguments); + RuntimeHash nextInserted = new RuntimeHash(); + arguments.push(nextInserted.createAnonymousReference()); + arguments.undefine(); + assertTrue(callerSlot.isActive()); + assertEquals(1, callerReferent.refCount); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, nextInserted.perlLifecycleState()); + caller.set(new RuntimeScalar()); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, callerReferent.perlLifecycleState()); + } + } + + @Test + void abandonedInnerFrameReleasesOnlyItsOwnRepeatedArgumentOwners() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar source = new RuntimeScalar(); + source.set(referent.createAnonymousReference()); + RuntimeList values = new RuntimeList(); + values.elements.add(source); + values.elements.add(source); + RuntimeCode.CallArguments outer = RuntimeCode.newCallArguments(2); + outer.add(values); + RuntimeCode.CallArguments inner = RuntimeCode.newCallArguments(2); + inner.add(values); + assertEquals(5, referent.refCount); + assertEquals(4, runtime.positiveOwnerSlotReferentCount(referent)); + RuntimeException failure = new RuntimeException("argument failed"); + assertSame(failure, inner.abandon(failure)); + assertEquals(3, referent.refCount); + assertEquals(2, runtime.positiveOwnerSlotReferentCount(referent)); + assertEquals(2, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + assertEquals(java.util.List.of(outer), runtime.executionState().callArgumentFrames); + assertTrue(runtime.lifecycleState.pendingOwnerReleases.isEmpty()); + inner.finish(); + assertEquals(3, referent.refCount); + outer.finish(); + MortalList.flushAboveMark(); + assertEquals(1, referent.refCount); + assertFalse(runtime.retainsPositiveOwnerSlotReferent(referent)); + assertTrue(runtime.executionState().callArgumentFrames.isEmpty()); + } + } + + @Test + void frameExitRetiresAbandonedStagingButPreservesItsCallersFrame() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeCode.CallArguments caller = RuntimeCode.newCallArguments(2); + RuntimeCode.pushArgs(new RuntimeArray()); + RuntimeCode.CallArguments abandoned = RuntimeCode.newCallArguments(2); + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar source = new RuntimeScalar(); + source.set(referent.createAnonymousReference()); + abandoned.add(source.getList()); + assertEquals(2, referent.refCount); + RuntimeCode.popArgs(); + assertEquals(java.util.List.of(caller), runtime.executionState().callArgumentFrames); + assertEquals(0, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + assertEquals(1, runtime.lifecycleState.pendingOwnerReleases.size()); + abandoned.finish(); + assertEquals(1, runtime.lifecycleState.pendingOwnerReleases.size()); + MortalList.flushAboveMark(); + assertEquals(1, referent.refCount); + caller.finish(); + } + } + + @Test + void nonLocalReturnKeepsItsMortalResultUntilTheReceiverConsumesIt() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeCode.CallArguments abandoned = RuntimeCode.newCallArguments(2); + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar source = new RuntimeScalar(); + source.set(referent.createAnonymousReference()); + RuntimeScalar returned = RuntimeScalar.copyToMortalReturnSlot(source); + PerlNonLocalReturnException transfer = new PerlNonLocalReturnException(returned); + assertSame(transfer, abandoned.abandon(transfer)); + assertTrue(referent.hasPendingReturnOwner()); + assertTrue(runtime.retainsPositiveOwnerSlotReferent(referent)); + assertEquals(2, referent.refCount); + RuntimeScalar receiver = new RuntimeScalar(); + receiver.set(returned); + assertFalse(referent.hasPendingReturnOwner()); + assertFalse(runtime.retainsPositiveOwnerSlotReferent(referent)); + assertEquals(2, referent.refCount); + assertSame(referent, receiver.value); + } + } + + @Test + void stagedArgumentRetainsItsChildrenUntilItsExplicitRelease() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash child = new RuntimeHash(); + RuntimeScalar childOwner = new RuntimeScalar(); + childOwner.set(child.createAnonymousReference()); + RuntimeScalar weakChild = new RuntimeScalar(); + weakChild.set(childOwner); + WeakRefRegistry.weaken(weakChild); + RuntimeHash parent = new RuntimeHash(); + // put consumes an element cell; the caller keeps its own SV. + parent.put("child", new RuntimeScalar(childOwner)); + RuntimeScalar parentOwner = new RuntimeScalar(); + parentOwner.set(parent.createAnonymousReference()); + RuntimeCode.CallArguments arguments = RuntimeCode.newCallArguments(2); + arguments.add(parentOwner.getList()); + MortalList.deferDecrementIfTracked(childOwner); + MortalList.deferDecrementIfTracked(parentOwner); + MortalList.flushAboveMark(); + ReachabilityWalker.sweepWeakRefs(true); + assertEquals(RuntimeScalarType.HASHREFERENCE, weakChild.type); + assertEquals(1, parent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + arguments.abandon(new RuntimeException("later argument failed")); + assertEquals(RuntimeScalarType.UNDEF, weakChild.type); + assertFalse(runtime.retainsPositiveOwnerSlotReferent(parent)); + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/PerlCallTransferOwnerTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/PerlCallTransferOwnerTest.java new file mode 100644 index 0000000000..4b93af94c2 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/PerlCallTransferOwnerTest.java @@ -0,0 +1,122 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class PerlCallTransferOwnerTest { + @Test + void scalarReturnOwnsItsReferentUntilTheReceivingCellConsumesIt() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar source = new RuntimeScalar(); + source.set(referent.createAnonymousReference()); + RuntimeList returned = RuntimeCode.coerceScalarCallResult( + source.getList(), RuntimeContextType.SCALAR); + RuntimeScalar value = (RuntimeScalar) returned.elements.getFirst(); + assertNotSame(source, value); + assertTrue(referent.hasPendingReturnOwner()); + assertEquals(PerlOwnerSlot.Kind.RETURN_VALUE, + runtime.lifecycleState.pendingOwnerReleases.getLast().ownerSlotRelease.kind()); + + MortalList.deferDecrementIfTracked(source); + RuntimeScalar receiver = new RuntimeScalar(); + receiver.set(value); + assertEquals(1, referent.refCount); + assertFalse(referent.hasPendingReturnOwner()); + assertTrue(receiver.countedOwnerSlot().isActive()); + assertEquals(RuntimeBase.PerlLifecycleState.LIVE, referent.perlLifecycleState()); + } + } + + @Test + void listReturnHasDistinctOwnersAndForwardingDoesNotDuplicateThem() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar source = new RuntimeScalar(); + source.set(referent.createAnonymousReference()); + RuntimeList input = new RuntimeList(); + input.elements.add(source); + input.elements.add(source); + RuntimeList returned = RuntimeCode.coerceScalarCallResult(input, RuntimeContextType.LIST); + assertNotSame(returned.elements.get(0), returned.elements.get(1)); + assertEquals(2, runtime.lifecycleState.pendingOwnerReleases.size()); + PerlOwnerSlot.PendingRelease first = runtime.lifecycleState.pendingOwnerReleases.get(0).ownerSlotRelease; + PerlOwnerSlot.PendingRelease second = runtime.lifecycleState.pendingOwnerReleases.get(1).ownerSlotRelease; + assertNotEquals(first.ownerSlotIdentity(), second.ownerSlotIdentity()); + assertEquals(PerlOwnerSlot.Kind.RETURN_VALUE, first.kind()); + assertEquals(PerlOwnerSlot.Kind.RETURN_VALUE, second.kind()); + assertSame(returned, RuntimeCode.coerceScalarCallResult(returned, RuntimeContextType.LIST)); + assertEquals(2, runtime.lifecycleState.pendingOwnerReleases.size()); + assertEquals(3, referent.refCount); + } + } + + @Test + void trackedCodeReturnGetsAnIndependentMortalOwner() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeCode code = new RuntimeCode((args, context) -> new RuntimeList(), null); + code.refCount = 0; + RuntimeScalar source = new RuntimeScalar(); + source.set(new RuntimeScalar(code)); + RuntimeList returned = RuntimeCode.coerceScalarCallResult(source.getList(), RuntimeContextType.SCALAR); + RuntimeScalar value = (RuntimeScalar) returned.elements.getFirst(); + assertNotSame(source, value); + assertSame(code, value.value); + assertTrue(code.hasPendingReturnOwner()); + MortalList.deferDecrementIfTracked(source); + RuntimeScalar receiver = new RuntimeScalar(); + receiver.set(value); + assertEquals(1, code.refCount); + assertFalse(code.hasPendingReturnOwner()); + assertSame(code, receiver.value); + } + } + + @Test + void stagedRepeatedValuesHaveIndependentArgumentOwnersAndFinishOnce() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar source = new RuntimeScalar(); + source.set(referent.createAnonymousReference()); + RuntimeList values = new RuntimeList(); + values.elements.add(source); + values.elements.add(source); + RuntimeCode.CallArguments frame = RuntimeCode.newCallArguments(2); + frame.add(values); + frame.add(new RuntimeScalar(1)); + assertEquals(2, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + assertEquals(3, referent.refCount); + frame.finish(); + assertEquals(0, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + assertEquals(2, runtime.lifecycleState.pendingOwnerReleases.size()); + for (RuntimeBase.PendingOwnerRelease pending : runtime.lifecycleState.pendingOwnerReleases) { + assertEquals(PerlOwnerSlot.Kind.ARGUMENT_VALUE, pending.ownerSlotRelease.kind()); + } + frame.finish(); + assertEquals(2, runtime.lifecycleState.pendingOwnerReleases.size()); + MortalList.flushAboveMark(); + assertEquals(1, referent.refCount); + assertTrue(source.countedOwnerSlot().isActive()); + } + } + + @Test + void lvalueReturnContinuesToBorrowTheOriginalCell() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar source = new RuntimeScalar(); + source.set(new RuntimeHash().createAnonymousReference()); + RuntimeList returned = RuntimeCode.coerceScalarCallResult( + source.getList(), RuntimeContextType.LVALUE, RuntimeContextType.LVALUE, false); + assertSame(source, returned.elements.getFirst()); + assertTrue(runtime.lifecycleState.pendingOwnerReleases.isEmpty()); + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/PerlClosureArgumentOwnerTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/PerlClosureArgumentOwnerTest.java new file mode 100644 index 0000000000..a691fcad62 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/PerlClosureArgumentOwnerTest.java @@ -0,0 +1,98 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class PerlClosureArgumentOwnerTest { + @Test + void temporaryClosureArgumentHasOneCountedOwnerUntilTheCallFrameExits() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeCode closure = new RuntimeCode((args, context) -> new RuntimeList(), null); + closure.refCount = 0; + closure.capturedScalars = new RuntimeScalar[0]; + RuntimeScalar argument = new RuntimeScalar(closure); + RuntimeCode.CallArguments staged = RuntimeCode.newCallArguments(1); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + MortalList.pushMark(); + boolean markActive = true; + boolean argsActive = false; + RuntimeArray perlArgs = null; + try { + staged.add(argument); + perlArgs = staged.toRuntimeArray(); + assertTrue(perlArgs.ownsElement(argument)); + assertEquals(1, closure.refCount); + assertEquals(1, closure.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + assertEquals(PerlOwnerSlot.Kind.ARGUMENT_VALUE, + argument.countedOwnerSlot().kind()); + + RuntimeCode.pushArgs(perlArgs); + argsActive = true; + RuntimeCode.popArgs(); + argsActive = false; + staged.finish(); + MortalList.popAndFlush(); + markActive = false; + + assertEquals(0, closure.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + assertFalse(perlArgs.ownsElement(argument)); + assertNull(closure.capturedScalars, + "the final argument release retires the temporary CV's captures"); + + ReachabilityQueryStats stats = measurement.stats(); + assertEquals(0, stats.rootQueries, + "argument release does not query Java graph reachability"); + assertEquals(0, stats.snapshotsBuilt, + "argument release does not build a reachability snapshot"); + } finally { + if (argsActive) RuntimeCode.popArgs(); + staged.finish(); + if (markActive) MortalList.popAndFlush(); + } + } + } + } + + @Test + void abandonedMaterializedArgumentsReleaseTheirCountedClosureOwner() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeCode closure = new RuntimeCode((args, context) -> new RuntimeList(), null); + closure.refCount = 0; + closure.capturedScalars = new RuntimeScalar[0]; + RuntimeScalar argument = new RuntimeScalar(closure); + RuntimeCode.CallArguments staged = RuntimeCode.newCallArguments(1); + + MortalList.pushMark(); + boolean markActive = true; + try { + staged.add(argument); + RuntimeArray perlArgs = staged.toRuntimeArray(); + assertEquals(1, closure.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + + RuntimeException failure = new RuntimeException("call rejected before entry"); + assertSame(failure, staged.abandon(failure)); + MortalList.popAndFlush(); + markActive = false; + + assertEquals(0, closure.ownerSlotCount(PerlOwnerSlot.Kind.ARGUMENT_VALUE)); + assertFalse(perlArgs.ownsElement(argument)); + assertNull(closure.capturedScalars, + "abandoning the call retires the temporary CV's captures"); + } finally { + staged.finish(); + if (markActive) MortalList.popAndFlush(); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/PerlDestroyFailureOwnershipTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/PerlDestroyFailureOwnershipTest.java new file mode 100644 index 0000000000..5131b4eda4 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/PerlDestroyFailureOwnershipTest.java @@ -0,0 +1,55 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@Tag("unit") +class PerlDestroyFailureOwnershipTest { + @Test + void throwingDestructorReleasesObserversAndOwnedChildrenWithSweepsSuppressed() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + runtime.lifecycleState.weakSweepSuppressionDepth++; + String className = "Issue1649::FailedOwnerCleanup"; + AtomicInteger calls = new AtomicInteger(); + RuntimeCode code = new RuntimeCode((args, context) -> { + calls.incrementAndGet(); + throw new PerlDieException(new RuntimeScalar("owner cleanup failed\n")); + }, null); + code.packageName = className; + code.subName = "DESTROY"; + GlobalVariable.getGlobalCodeRef(className + "::DESTROY").set(new RuntimeScalar(code)); + try { + RuntimeHash child = new RuntimeHash(); + RuntimeHash parent = new RuntimeHash(); + parent.put("child", child.createAnonymousReference()); + parent.blessId = NameNormalizer.getBlessId(className); + RuntimeScalar owner = new RuntimeScalar(); + owner.set(parent.createAnonymousReference()); + RuntimeScalar weakParent = new RuntimeScalar(); + weakParent.set(owner); + WeakRefRegistry.weaken(weakParent); + RuntimeScalar weakChild = child.createAnonymousReference(); + WeakRefRegistry.weaken(weakChild); + + owner.set(new RuntimeScalar()); + + assertEquals(1, calls.get()); + assertEquals(RuntimeScalarType.UNDEF, weakParent.type); + assertEquals(RuntimeScalarType.UNDEF, weakChild.type); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, parent.perlLifecycleState()); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, child.perlLifecycleState()); + assertEquals(Integer.MIN_VALUE, parent.refCount); + assertFalse(parent.currentlyDestroying); + } finally { + GlobalVariable.removeGlobalCodeRefForStashDelete(className + "::DESTROY"); + runtime.lifecycleState.weakSweepSuppressionDepth--; + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/PerlFinalWeakOwnerTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/PerlFinalWeakOwnerTest.java new file mode 100644 index 0000000000..3ace44129e --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/PerlFinalWeakOwnerTest.java @@ -0,0 +1,187 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class PerlFinalWeakOwnerTest { + @Test + void returnOwnerAcquiresTheScalarCellBeforeItsLexicalBindingExits() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash contents = new RuntimeHash(); + RuntimeScalar cell = new RuntimeScalar(); + MyVarCleanupStack.register(cell); + cell.set(contents.createAnonymousReference()); + RuntimeBase returned = RuntimeCode.prepareScopeReturn( + cell.createReference(), RuntimeContextType.SCALAR, true); + assertEquals(1, cell.refCount); + MortalList.deferDecrementIfNotCaptured(cell); + MyVarCleanupStack.unregister(cell); + assertFalse(cell.localBindingExists); + assertTrue(cell.refCountOwned); + assertEquals(1, contents.refCount); + RuntimeScalar receiver = new RuntimeScalar(); + receiver.set(returned.scalar()); + assertEquals(1, cell.refCount); + assertSame(cell, receiver.value); + cell.set(new RuntimeScalar()); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, contents.perlLifecycleState()); + receiver.set(new RuntimeScalar()); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, cell.perlLifecycleState()); + } + } + + @Test + void finalNestedArraySlotOverwriteReleasesWithoutRequestingASweep() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + runtime.lifecycleState.weakSweepSuppressionDepth++; + RuntimeCode.pushArgs(new RuntimeArray()); + RuntimeCode.pushArgs(new RuntimeArray()); + try { + RuntimeHash referent = new RuntimeHash(); + referent.blessId = NameNormalizer.getBlessId("FinalWeakOwner::NoDestroy"); + RuntimeArray array = new RuntimeArray(); + array.push(referent.createAnonymousReference()); + RuntimeScalar element = array.elements.getFirst(); + RuntimeScalar observer = new RuntimeScalar(); + observer.set(element); + WeakRefRegistry.weaken(observer); + PerlOwnerSlot slot = element.countedOwnerSlot(); + try (MortalList.ReachabilityQueryMeasurement measurement = MortalList.measureReachabilityQueries()) { + element.set(new RuntimeScalar()); + assertEquals(RuntimeScalarType.UNDEF, observer.type); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, referent.perlLifecycleState()); + assertFalse(slot.isActive()); + assertTrue(runtime.lifecycleState.targetedWeakSweepReferents.isEmpty()); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } finally { + RuntimeCode.popArgs(); + RuntimeCode.popArgs(); + runtime.lifecycleState.weakSweepSuppressionDepth--; + } + } + } + + @Test + void weakeningTheLastIdentifiedOwnerDoesNotConsultReachability() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + runtime.lifecycleState.weakSweepSuppressionDepth++; + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar owner = new RuntimeScalar(); + owner.set(referent.createAnonymousReference()); + PerlOwnerSlot slot = owner.countedOwnerSlot(); + try (MortalList.ReachabilityQueryMeasurement measurement = MortalList.measureReachabilityQueries()) { + WeakRefRegistry.weaken(owner); + assertEquals(RuntimeScalarType.UNDEF, owner.type); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, referent.perlLifecycleState()); + assertFalse(slot.isActive()); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + assertEquals(0, measurement.stats().scalarReachabilityQueries); + } finally { + runtime.lifecycleState.weakSweepSuppressionDepth--; + } + } + } + + @Test + void weakeningOneLexicalOwnerTrustsAnotherExplicitOwnerSlot() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash referent = new RuntimeHash(); + referent.blessId = NameNormalizer.getBlessId("FinalWeakOwner::Shared"); + RuntimeScalar observer = referent.createAnonymousReference(); + WeakRefRegistry.weaken(observer); + + RuntimeScalar weakenedOwner = new RuntimeScalar(); + RuntimeScalar survivingOwner = new RuntimeScalar(); + MyVarCleanupStack.register(weakenedOwner); + MyVarCleanupStack.register(survivingOwner); + weakenedOwner.set(referent.createAnonymousReference()); + survivingOwner.set(referent.createAnonymousReference()); + + try { + assertEquals(2, referent.countedOwnerSlotCount()); + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + WeakRefRegistry.weaken(weakenedOwner); + + assertTrue(WeakRefRegistry.isweak(observer)); + assertTrue(WeakRefRegistry.isweak(weakenedOwner)); + assertEquals(1, referent.countedOwnerSlotCount()); + assertEquals(0, measurement.stats().scalarReachabilityQueries, + "the surviving owner slot is direct liveness evidence"); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } finally { + if (WeakRefRegistry.isweak(weakenedOwner)) { + WeakRefRegistry.unweaken(weakenedOwner); + } + if (WeakRefRegistry.isweak(observer)) WeakRefRegistry.unweaken(observer); + MyVarCleanupStack.unregister(weakenedOwner); + MyVarCleanupStack.unregister(survivingOwner); + weakenedOwner.set(new RuntimeScalar()); + survivingOwner.set(new RuntimeScalar()); + observer.set(new RuntimeScalar()); + } + } + } + + @Test + void scalarCellsReleaseTheirWeakObserversWhenTheLexicalOwnerExits() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + runtime.lifecycleState.weakSweepSuppressionDepth++; + RuntimeScalar cell = new RuntimeScalar(42); + MyVarCleanupStack.register(cell); + RuntimeScalar observer = new RuntimeScalar(); + observer.set(cell.createReference()); + WeakRefRegistry.weaken(observer); + assertEquals(0, cell.refCount); + assertTrue(cell.localBindingExists); + assertEquals(RuntimeScalarType.REFERENCE, observer.type); + RuntimeScalar.scopeExitCleanup(cell); + MyVarCleanupStack.unregister(cell); + assertFalse(cell.localBindingExists); + assertEquals(RuntimeScalarType.UNDEF, observer.type); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, cell.perlLifecycleState()); + RuntimeScalar.scopeExitCleanup(cell); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, cell.perlLifecycleState()); + runtime.lifecycleState.weakSweepSuppressionDepth--; + } + } + + @Test + void escapedReferenceOutlivesTheLexicalBindingAndOwnsTheCellsContents() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash contents = new RuntimeHash(); + RuntimeScalar cell = new RuntimeScalar(); + cell.set(contents.createAnonymousReference()); + MyVarCleanupStack.register(cell); + RuntimeScalar escaped = new RuntimeScalar(); + escaped.set(cell.createReference()); + RuntimeScalar observer = new RuntimeScalar(); + observer.set(escaped); + WeakRefRegistry.weaken(observer); + RuntimeScalar.scopeExitCleanup(cell); + MyVarCleanupStack.unregister(cell); + assertEquals(RuntimeScalarType.REFERENCE, observer.type); + assertTrue(cell.refCountOwned); + assertEquals(1, cell.refCount); + assertEquals(1, contents.refCount); + escaped.set(new RuntimeScalar()); + assertEquals(RuntimeScalarType.UNDEF, observer.type); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, cell.perlLifecycleState()); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, contents.perlLifecycleState()); + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/PerlOwnerSlotLegacyBridgeTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/PerlOwnerSlotLegacyBridgeTest.java index 1d1751eaf5..e45e507b25 100644 --- a/src/test/java/org/perlonjava/runtime/runtimetypes/PerlOwnerSlotLegacyBridgeTest.java +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/PerlOwnerSlotLegacyBridgeTest.java @@ -43,6 +43,8 @@ void queuedCaptureReleaseRetainsSlotProvenanceUntilDrain() { RuntimeHash referent = new RuntimeHash(); referent.refCount = 0; referent.localBindingExists = true; + int lexicalMark = MyVarCleanupStack.pushMark(); + MyVarCleanupStack.register(referent); PerlOwnerSlot slot = new PerlOwnerSlot(PerlOwnerSlot.Kind.CLOSURE_PAD); slot.acquireLegacyCapture(referent); @@ -65,6 +67,7 @@ void queuedCaptureReleaseRetainsSlotProvenanceUntilDrain() { assertEquals(0, referent.refCount); assertTrue(release.isCompleted()); MortalList.popMark(); + MyVarCleanupStack.popMark(lexicalMark); } } } diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/PerlOwnerSlotSemanticOwnerTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/PerlOwnerSlotSemanticOwnerTest.java new file mode 100644 index 0000000000..a4625fefd2 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/PerlOwnerSlotSemanticOwnerTest.java @@ -0,0 +1,123 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class PerlOwnerSlotSemanticOwnerTest { + @Test + void scalarCellSlotCanAcquireBorrowReplaceAndRelease() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash first = new RuntimeHash(); + RuntimeHash second = new RuntimeHash(); + PerlOwnerSlot slot = new PerlOwnerSlot(PerlOwnerSlot.Kind.SCALAR_CELL); + + slot.acquireNativeOwner(first); + + assertSame(first, slot.borrowReferent()); + assertTrue(slot.isNativeOwner()); + assertEquals(1, first.nativeOwnerSlotCount()); + assertEquals(1, runtime.positiveOwnerSlotReferentCount(first)); + + slot.replaceNativeOwner(second); + + assertEquals(0, first.nativeOwnerSlotCount()); + assertEquals(0, runtime.positiveOwnerSlotReferentCount(first)); + assertSame(second, slot.borrowReferent()); + assertEquals(1, second.nativeOwnerSlotCount()); + assertEquals(1, runtime.positiveOwnerSlotReferentCount(second)); + + slot.release(); + + assertNull(slot.borrowReferent()); + assertFalse(slot.isNativeOwner()); + assertTrue(slot.usesNativeOwnerLedger()); + assertEquals(0, second.nativeOwnerSlotCount()); + assertEquals(0, runtime.positiveOwnerSlotReferentCount(second)); + } + } + + @Test + void eachClosureCaptureOwnsItsScalarPadCellIndependently() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar padCell = new RuntimeScalar(); + + PerlOwnerSlot first = padCell.retainClosureCaptureSlot(); + PerlOwnerSlot second = padCell.retainClosureCaptureSlot(); + + assertNotEquals(first.identity(), second.identity()); + assertEquals(2, padCell.captureCount); + assertTrue(first.isActive()); + assertTrue(second.isActive()); + assertEquals(2, padCell.ownerSlotCount(PerlOwnerSlot.Kind.SCALAR_CAPTURE)); + assertEquals(0, runtime.positiveOwnerSlotReferentCount(padCell)); + + padCell.releaseClosureCaptureSlot(first); + + assertEquals(1, padCell.captureCount); + assertFalse(first.isActive()); + assertTrue(second.isActive()); + assertEquals(1, padCell.ownerSlotCount(PerlOwnerSlot.Kind.SCALAR_CAPTURE)); + assertFalse(runtime.retainsPositiveOwnerSlotReferent(padCell)); + + padCell.releaseClosureCaptureSlot(second); + + assertEquals(0, padCell.captureCount); + assertFalse(second.isActive()); + assertEquals(0, padCell.ownerSlotCount(PerlOwnerSlot.Kind.SCALAR_CAPTURE)); + assertFalse(runtime.retainsPositiveOwnerSlotReferent(padCell)); + } + } + + @Test + void aggregateSlotKindsUseIndependentSemanticOwners() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash referent = new RuntimeHash(); + PerlOwnerSlot arraySlot = new PerlOwnerSlot(PerlOwnerSlot.Kind.ARRAY_ELEMENT); + PerlOwnerSlot hashSlot = new PerlOwnerSlot(PerlOwnerSlot.Kind.HASH_ELEMENT); + + arraySlot.acquireNativeOwner(referent); + hashSlot.acquireNativeOwner(referent); + + assertEquals(1, referent.ownerSlotCount(PerlOwnerSlot.Kind.ARRAY_ELEMENT)); + assertEquals(1, referent.ownerSlotCount(PerlOwnerSlot.Kind.HASH_ELEMENT)); + assertEquals(2, referent.nativeOwnerSlotCount()); + assertEquals(2, runtime.positiveOwnerSlotReferentCount(referent)); + + arraySlot.release(); + assertEquals(1, referent.nativeOwnerSlotCount()); + assertTrue(runtime.retainsPositiveOwnerSlotReferent(referent)); + + hashSlot.release(); + assertEquals(0, referent.nativeOwnerSlotCount()); + assertFalse(runtime.retainsPositiveOwnerSlotReferent(referent)); + } + } + + @Test + void nativeOwnerCannotReuseADeadReferentOrMigratePendingLegacyReleases() { + RuntimeHash destroyed = new RuntimeHash(); + destroyed.refCount = Integer.MIN_VALUE; + PerlOwnerSlot deadSlot = new PerlOwnerSlot(PerlOwnerSlot.Kind.SCALAR_CELL); + assertThrows(IllegalStateException.class, () -> deadSlot.acquireNativeOwner(destroyed)); + + RuntimeHash live = new RuntimeHash(); + live.refCount = 0; + PerlOwnerSlot pendingSlot = new PerlOwnerSlot(PerlOwnerSlot.Kind.CLOSURE_PAD); + pendingSlot.acquireLegacyCapture(live); + assertThrows(IllegalStateException.class, () -> pendingSlot.acquireNativeOwner(live)); + assertEquals(1, live.refCount); + assertEquals(1, pendingSlot.legacyCaptureCount()); + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/PerlStrongOwnerProofTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/PerlStrongOwnerProofTest.java new file mode 100644 index 0000000000..4a34712226 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/PerlStrongOwnerProofTest.java @@ -0,0 +1,50 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class PerlStrongOwnerProofTest { + @Test + void excludesTemporaryRootsButAcceptsCountedAndUntrackedOwners() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + RuntimeHash target = new RuntimeHash(); + assertFalse(target.hasPerlStrongOwner()); + + PerlOwnerSlot temporaryOwner = new PerlOwnerSlot(PerlOwnerSlot.Kind.TEMPORARY_ROOT); + temporaryOwner.acquireTemporaryRootIdentity(target); + try { + assertFalse(target.hasPerlStrongOwner(), + "a temporary evaluation root is not a Perl owner"); + } finally { + temporaryOwner.release(); + } + + RuntimeScalar countedOwner = new RuntimeScalar(); + countedOwner.set(target.createAnonymousReference()); + assertTrue(target.hasPerlStrongOwner()); + countedOwner.set(new RuntimeScalar()); + assertFalse(target.hasPerlStrongOwner()); + + RuntimeHash untrackedTarget = new RuntimeHash(); + RuntimeArray container = new RuntimeArray(); + RuntimeScalar untrackedOwner = referenceTo(untrackedTarget); + container.elements.add(untrackedOwner); + untrackedOwner.markContainerOwner(container); + untrackedOwner.refreshUntrackedReferenceOwner(); + assertTrue(untrackedTarget.hasPerlStrongOwner()); + untrackedOwner.set(new RuntimeScalar()); + assertFalse(untrackedTarget.hasPerlStrongOwner()); + } + } + + private static RuntimeScalar referenceTo(RuntimeBase referent) { + RuntimeScalar scalar = new RuntimeScalar(); + scalar.type = RuntimeScalarType.REFERENCE; + scalar.value = referent; + return scalar; + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/PromotedUntrackedOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/PromotedUntrackedOwnerSlotTest.java new file mode 100644 index 0000000000..626c859f5b --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/PromotedUntrackedOwnerSlotTest.java @@ -0,0 +1,91 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class PromotedUntrackedOwnerSlotTest { + @Test + void scalarInUnownedTemporaryArrayDoesNotPinPromotedReferent() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar target = new RuntimeScalar("temporary array referent"); + RuntimeScalar element = referenceTo(target); + RuntimeArray temporaryArray = new RuntimeArray(); + temporaryArray.elements.add(element); + element.markContainerOwner(temporaryArray); + element.refreshUntrackedReferenceOwner(); + PerlOwnerSlot ownerSlot = element.untrackedReferenceOwnerSlot(); + assertNotNull(ownerSlot); + + target.refCount = 0; + + assertFalse(target.hasUntrackedReferenceOwnerSlotIdentity()); + assertFalse(ownerSlot.isActive(), + "a temporary container without a Perl owner must not retain the referent"); + assertEquals(0, target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + } + } + + @Test + void durableUncountedPackageValueSurvivesReferentPromotionAndFinalCountedRelease() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar target = new RuntimeScalar("promoted referent"); + RuntimeScalar packageOwner = referenceTo(target); + String globalName = "PromotedUntrackedOwnerSlotTest::packageOwner"; + GlobalVariable.globalVariables.put(globalName, packageOwner); + PerlOwnerSlot ownerSlot = packageOwner.untrackedReferenceOwnerSlot(); + assertNotNull(ownerSlot, + "a durable owner without a count needs an owner-slot identity"); + assertEquals(target, ownerSlot.referent()); + + RuntimeScalar countedOwner = new RuntimeScalar(); + countedOwner.set(target.createReference()); + packageOwner.refreshUntrackedReferenceOwner(); + RuntimeScalar weak = target.createReference(); + WeakRefRegistry.weaken(weak); + + try { + assertEquals(1, target.refCount, + "a later counted store promotes the referent independently"); + assertTrue(ownerSlot.isActive(), + "promotion must not retire the earlier uncounted Perl slot"); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + countedOwner.set(new RuntimeScalar()); + + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, target.refCount); + assertTrue(WeakRefRegistry.isweak(weak), + "the surviving package scalar keeps the weak referent alive"); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + GlobalVariable.globalVariables.remove(globalName); + + assertFalse(ownerSlot.isActive()); + assertFalse(WeakRefRegistry.isweak(weak)); + assertEquals(RuntimeScalarType.UNDEF, weak.type); + assertEquals(Integer.MIN_VALUE, target.refCount); + } finally { + GlobalVariable.globalVariables.remove(globalName); + if (WeakRefRegistry.isweak(weak)) WeakRefRegistry.unweaken(weak); + countedOwner.set(new RuntimeScalar()); + } + } + } + + private static RuntimeScalar referenceTo(RuntimeBase referent) { + RuntimeScalar reference = new RuntimeScalar(); + reference.type = RuntimeScalarType.REFERENCE; + reference.value = referent; + return reference; + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeCodeStashOwnerReleaseTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeCodeStashOwnerReleaseTest.java new file mode 100644 index 0000000000..6c39782fe4 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeCodeStashOwnerReleaseTest.java @@ -0,0 +1,53 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class RuntimeCodeStashOwnerReleaseTest { + @Test + void activeCallReleasesStateAfterLastStashOwnerLeavesDespiteStaleBridgeCount() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + String name = "RuntimeCodeStashOwnerReleaseTest::active"; + RuntimeCode code = new RuntimeCode((args, context) -> new RuntimeList(), null); + code.hadStashRef = true; + code.stashRefCount = 1; + + RuntimeScalar stashSlot = new RuntimeScalar(); + stashSlot.type = RuntimeScalarType.CODE; + stashSlot.value = code; + GlobalVariable.globalCodeRefs.put(name, stashSlot); + + RuntimeScalar weak = new RuntimeScalar(); + weak.type = RuntimeScalarType.CODE; + weak.value = code; + WeakRefRegistry.weaken(weak); + // The bridge count can outlive its Perl owners during the staged + // migration; the owner ledger must decide final state cleanup. + code.refCount = 5; + + PerlOwnerSlot activeCall = + new PerlOwnerSlot(PerlOwnerSlot.Kind.ACTIVE_CODE); + activeCall.acquireActiveCodeIdentity(code); + + try { + GlobalVariable.removeGlobalCodeRefForStashDelete(name); + assertTrue(WeakRefRegistry.isweak(weak), + "the active call keeps CODE state alive until it returns"); + + activeCall.release(); + + assertEquals(RuntimeScalarType.UNDEF, weak.type, + "the active call releases Perl state despite a stale bridge count"); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, + code.perlLifecycleState()); + } finally { + if (activeCall.isActive()) activeCall.release(); + GlobalVariable.globalCodeRefs.remove(name); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarContainerOwnerConsistencyTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarContainerOwnerConsistencyTest.java new file mode 100644 index 0000000000..54bdda1570 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarContainerOwnerConsistencyTest.java @@ -0,0 +1,213 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +@Tag("unit") +class RuntimeScalarContainerOwnerConsistencyTest { + @Test + void stalePreferredOwnerIsReplacedByAStillStoredContainer() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar value = new RuntimeHash().createAnonymousReference(); + RuntimeArray survivingOwner = new RuntimeArray(); + RuntimeArray removedOwner = new RuntimeArray(); + + survivingOwner.elements.add(value); + removedOwner.elements.add(value); + assertSame(removedOwner, value.getContainerOwner()); + + removedOwner.elements.remove(value); + + assertSame(survivingOwner, value.getContainerOwner()); + } + } + + @Test + void packageRootOwnerSurvivesWhenCurrentOwnerChanges() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar value = new RuntimeHash().createAnonymousReference(); + RuntimeArray packageRoot = new RuntimeArray(); + RuntimeArray otherOwner = new RuntimeArray(); + + packageRoot.elements.add(value); + GlobalVariable.markPackageGlobalRoot(packageRoot); + otherOwner.elements.add(value); + assertSame(otherOwner, value.getContainerOwner()); + + assertNotNull(value.untrackedReferenceOwnerSlot()); + } + } + + @Test + void slotRemovalRetiresOnlyTheEdgesThatNoLongerExist() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar value = new RuntimeHash().createAnonymousReference(); + RuntimeArray array = new RuntimeArray(); + array.elements.add(value); + assertSame(array, value.getContainerOwner()); + + array.elements.remove(value); + assertNull(value.getContainerOwner()); + + RuntimeHash hash = new RuntimeHash(); + hash.elements.put("first", value); + hash.elements.put("second", value); + hash.elements.remove("first"); + assertSame(hash, value.getContainerOwner()); + + hash.elements.remove("second"); + assertNull(value.getContainerOwner()); + } + } + + @Test + void dynamicStateRestoreRebindsOwnersToTheLiveAggregate() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar arrayValue = new RuntimeHash().createAnonymousReference(); + RuntimeArray array = new RuntimeArray(); + array.elements.add(arrayValue); + array.dynamicSaveState(); + array.dynamicRestoreState(); + assertSame(array, arrayValue.getContainerOwner()); + + RuntimeScalar hashValue = new RuntimeHash().createAnonymousReference(); + RuntimeHash hash = new RuntimeHash(); + hash.elements.put("value", hashValue); + hash.dynamicSaveState(); + hash.dynamicRestoreState(); + assertSame(hash, hashValue.getContainerOwner()); + } + } + + @Test + void arrayIteratorRemovalRetiresTheContainerOwner() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar value = new RuntimeHash().createAnonymousReference(); + RuntimeArray array = new RuntimeArray(); + array.elements.add(value); + + var iterator = array.elements.iterator(); + assertSame(value, iterator.next()); + iterator.remove(); + + assertNull(value.getContainerOwner()); + } + } + + @Test + void arrayBulkRemovalRetiresTheContainerOwner() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar value = new RuntimeHash().createAnonymousReference(); + RuntimeArray array = new RuntimeArray(); + array.elements.add(value); + + array.elements.removeAll(java.util.List.of(value)); + + assertNull(value.getContainerOwner()); + } + } + + @Test + void arrayBulkReplacementUpdatesContainerOwners() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar oldValue = new RuntimeHash().createAnonymousReference(); + RuntimeScalar newValue = new RuntimeHash().createAnonymousReference(); + RuntimeArray array = new RuntimeArray(); + array.elements.add(oldValue); + + array.elements.replaceAll(ignoredValue -> newValue); + + assertNull(oldValue.getContainerOwner()); + assertSame(array, newValue.getContainerOwner()); + } + } + + @Test + void hashEntryViewRemovalRetiresTheContainerOwner() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar value = new RuntimeHash().createAnonymousReference(); + RuntimeHash hash = new RuntimeHash(); + hash.elements.put("value", value); + + var iterator = hash.elements.entrySet().iterator(); + assertSame(value, iterator.next().getValue()); + iterator.remove(); + + assertNull(value.getContainerOwner()); + } + } + + @Test + void hashEntryReplacementUpdatesBothContainerOwners() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar oldValue = new RuntimeHash().createAnonymousReference(); + RuntimeScalar newValue = new RuntimeHash().createAnonymousReference(); + RuntimeHash hash = new RuntimeHash(); + hash.elements.put("value", oldValue); + + hash.elements.entrySet().iterator().next().setValue(newValue); + + assertNull(oldValue.getContainerOwner()); + assertSame(hash, newValue.getContainerOwner()); + } + } + + @Test + void hashValuesViewRemovalRetiresTheContainerOwner() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar value = new RuntimeHash().createAnonymousReference(); + RuntimeHash hash = new RuntimeHash(); + hash.elements.put("value", value); + + hash.elements.values().remove(value); + + assertNull(value.getContainerOwner()); + } + } + + @Test + void hashKeyViewRemovalRetiresTheContainerOwner() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar value = new RuntimeHash().createAnonymousReference(); + RuntimeHash hash = new RuntimeHash(); + hash.elements.put("value", value); + + var iterator = hash.elements.keySet().iterator(); + assertSame("value", iterator.next()); + iterator.remove(); + + assertNull(value.getContainerOwner()); + } + } + + @Test + void hashComputeReplacementUpdatesContainerOwners() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar oldValue = new RuntimeHash().createAnonymousReference(); + RuntimeScalar newValue = new RuntimeHash().createAnonymousReference(); + RuntimeHash hash = new RuntimeHash(); + hash.elements.put("value", oldValue); + + hash.elements.computeIfPresent("value", (key, value) -> newValue); + + assertNull(oldValue.getContainerOwner()); + assertSame(hash, newValue.getContainerOwner()); + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarCountedOwnerRepairTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarCountedOwnerRepairTest.java new file mode 100644 index 0000000000..d1f9eeef3e --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarCountedOwnerRepairTest.java @@ -0,0 +1,57 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class RuntimeScalarCountedOwnerRepairTest { + @Test + void survivingArraySlotRepairsNonPackageWeakReferentWithoutRootWalk() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash target = new RuntimeHash(); + target.blessId = NameNormalizer.getBlessId("RuntimeScalarCountedOwnerRepairTest::Target"); + RuntimeArray arrayOwner = new RuntimeArray(); + arrayOwner.push(target.createAnonymousReference()); + + RuntimeScalar scalarOwner = new RuntimeScalar(); + scalarOwner.set(target.createAnonymousReference()); + RuntimeScalar observer = target.createAnonymousReference(); + WeakRefRegistry.weaken(observer); + + try { + assertFalse(target.storedInPackageGlobal); + assertFalse(target.localBindingExists); + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.ARRAY_ELEMENT)); + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.SCALAR_CELL)); + assertTrue(WeakRefRegistry.isweak(observer)); + + // Model a transient selective-count undercount while retaining + // both explicit owner identities. + target.refCount = 1; + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + scalarOwner.set(new RuntimeScalar()); + + assertEquals(1, target.refCount); + assertEquals(1, target.countedOwnerSlotCount()); + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.ARRAY_ELEMENT)); + assertTrue(WeakRefRegistry.isweak(observer)); + assertEquals(0, measurement.stats().rootQueries, + "the surviving array slot repairs the transient zero"); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + arrayOwner.elements.get(0).set(new RuntimeScalar()); + assertFalse(WeakRefRegistry.isweak(observer), + "releasing the final counted slot clears the weak observer"); + } finally { + if (WeakRefRegistry.isweak(observer)) WeakRefRegistry.unweaken(observer); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarOwnerSlotTest.java new file mode 100644 index 0000000000..d3892b05c0 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarOwnerSlotTest.java @@ -0,0 +1,242 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class RuntimeScalarOwnerSlotTest { + @Test + void independentScalarCellsKeepTheirIdentitiesAcrossOverwriteAndSameReferentAssignment() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash first = new RuntimeHash(); + RuntimeHash replacement = new RuntimeHash(); + RuntimeScalar left = new RuntimeScalar(); + RuntimeScalar right = new RuntimeScalar(); + RuntimeScalar reference = first.createReference(); + + left.set(reference); + right.set(reference); + PerlOwnerSlot leftSlot = left.countedOwnerSlot(); + PerlOwnerSlot rightSlot = right.countedOwnerSlot(); + + assertNotEquals(leftSlot.identity(), rightSlot.identity()); + assertEquals(2, first.ownerSlotCount(PerlOwnerSlot.Kind.SCALAR_CELL)); + assertEquals(2, first.refCount); + assertEquals(0, runtime.positiveOwnerSlotReferentCount(first)); + + left.set(reference); + assertSame(leftSlot, left.countedOwnerSlot()); + assertSame(first, leftSlot.referent()); + assertEquals(2, first.ownerSlotCount(PerlOwnerSlot.Kind.SCALAR_CELL)); + assertEquals(2, first.refCount); + + left.set(replacement.createReference()); + assertSame(leftSlot, left.countedOwnerSlot()); + assertSame(replacement, leftSlot.referent()); + assertSame(first, rightSlot.referent()); + assertEquals(1, first.ownerSlotCount(PerlOwnerSlot.Kind.SCALAR_CELL)); + assertEquals(1, replacement.ownerSlotCount(PerlOwnerSlot.Kind.SCALAR_CELL)); + assertEquals(1, first.refCount); + + left.set(new RuntimeScalar()); + assertFalse(leftSlot.isActive()); + assertEquals(0, replacement.ownerSlotCount(PerlOwnerSlot.Kind.SCALAR_CELL)); + assertTrue(rightSlot.isActive()); + } + } + + @Test + void arrayRemovalCarriesTheRemovedElementsIdentityUntilItsDecrementDrains() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + MortalList.setActive(true); + MortalList.pushMark(); + RuntimeHash target = new RuntimeHash(); + RuntimeArray array = new RuntimeArray(); + array.push(target.createReference()); + array.push(target.createReference()); + PerlOwnerSlot retainedSlot = array.elements.get(0).countedOwnerSlot(); + PerlOwnerSlot removedSlot = array.elements.get(1).countedOwnerSlot(); + + assertNotEquals(retainedSlot.identity(), removedSlot.identity()); + assertEquals(2, target.ownerSlotCount(PerlOwnerSlot.Kind.ARRAY_ELEMENT)); + assertEquals(0, runtime.positiveOwnerSlotReferentCount(target)); + + RuntimeScalar removed = RuntimeArray.pop(array); + RuntimeBase.PendingOwnerRelease pending = runtime.lifecycleState.pendingOwnerReleases.getLast(); + PerlOwnerSlot.PendingRelease release = pending.ownerSlotRelease; + + assertNotNull(release); + assertEquals(removedSlot.identity(), release.ownerSlotIdentity()); + assertEquals(PerlOwnerSlot.Kind.ARRAY_ELEMENT, release.kind()); + assertFalse(release.isCompleted()); + assertFalse(removed.refCountOwned); + assertFalse(removedSlot.isActive()); + assertTrue(retainedSlot.isActive()); + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.ARRAY_ELEMENT)); + assertEquals(2, target.refCount); + + MortalList.flushAboveMark(); + assertTrue(release.isCompleted()); + assertEquals(1, target.refCount); + assertTrue(retainedSlot.isActive()); + MortalList.popMark(); + } + } + + @Test + void queuedReleaseCannotReleaseANewerValueStoredInTheSameCell() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + MortalList.setActive(true); + RuntimeHash first = new RuntimeHash(); + RuntimeHash next = new RuntimeHash(); + first.localBindingExists = true; + RuntimeScalar cell = new RuntimeScalar(); + cell.set(first.createReference()); + PerlOwnerSlot slot = cell.countedOwnerSlot(); + + MortalList.pushMark(); + MortalList.deferDecrementIfTracked(cell); + PerlOwnerSlot.PendingRelease release = + runtime.lifecycleState.pendingOwnerReleases.getLast().ownerSlotRelease; + assertNotNull(release); + assertEquals(slot.identity(), release.ownerSlotIdentity()); + + boolean previousSuppression = MortalList.suppressFlush(true); + try { + cell.set(next.createReference()); + } finally { + MortalList.suppressFlush(previousSuppression); + } + assertSame(slot, cell.countedOwnerSlot()); + assertSame(next, slot.referent()); + + MortalList.flushAboveMark(); + assertTrue(release.isCompleted()); + assertSame(next, slot.referent()); + assertEquals(1, next.ownerSlotCount(PerlOwnerSlot.Kind.SCALAR_CELL)); + assertEquals(0, first.ownerSlotCount(PerlOwnerSlot.Kind.SCALAR_CELL)); + MortalList.popMark(); + } + } + + @Test + void hashKeysOwnDistinctElementSlotsEvenWhenTheyStoreTheSameInputScalar() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash target = new RuntimeHash(); + RuntimeHash hash = new RuntimeHash(); + RuntimeScalar reference = target.createReference(); + hash.put("left", reference); + hash.put("right", reference); + + PerlOwnerSlot left = hash.elements.get("left").countedOwnerSlot(); + PerlOwnerSlot right = hash.elements.get("right").countedOwnerSlot(); + assertNotEquals(left.identity(), right.identity()); + assertEquals(PerlOwnerSlot.Kind.HASH_ELEMENT, left.kind()); + assertEquals(PerlOwnerSlot.Kind.HASH_ELEMENT, right.kind()); + assertEquals(2, target.ownerSlotCount(PerlOwnerSlot.Kind.HASH_ELEMENT)); + + WeakRefRegistry.weaken(hash.elements.get("left")); + assertFalse(left.isActive()); + assertTrue(right.isActive()); + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.HASH_ELEMENT)); + assertEquals(1, target.refCount); + } + } + + @Test + void overwritingAWeakHashSlotDoesNotReleaseItsTargetsChildOwners() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + MortalList.setActive(true); + RuntimeHash child = new RuntimeHash(); + RuntimeHash parent = new RuntimeHash(); + parent.put("child", child.createReference()); + RuntimeScalar strongParent = new RuntimeScalar(); + strongParent.set(parent.createReference()); + RuntimeScalar weakChild = child.createReference(); + WeakRefRegistry.weaken(weakChild); + + RuntimeHash observer = new RuntimeHash(); + observer.put("parent", parent.createReference()); + WeakRefRegistry.weaken(observer.elements.get("parent")); + PerlOwnerSlot childSlot = parent.elements.get("child").countedOwnerSlot(); + + observer.put("parent", new RuntimeScalar()); + + assertTrue(childSlot.isActive()); + assertTrue(parent.elements.get("child").refCountOwned); + assertEquals(1, child.refCount); + assertTrue(strongParent.countedOwnerSlot().isActive()); + } + } + + @Test + void finalElementOwnerClearsWeakReferencesWithoutConsultingAStaleRootSnapshot() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + MortalList.setActive(true); + runtime.lifecycleState.weakSweepSuppressionDepth++; + int lexicalMark = MyVarCleanupStack.pushMark(); + try { + RuntimeArray child = new RuntimeArray(); + RuntimeScalar strong = new RuntimeScalar(); + strong.set(child.createAnonymousReference()); + RuntimeScalar weak = new RuntimeScalar(); + weak.set(strong); + WeakRefRegistry.weaken(weak); + RuntimeArray metadata = new RuntimeArray(); + metadata.push(strong); + RuntimeScalar metadataCell = new RuntimeScalar(); + metadataCell.set(metadata.createAnonymousReference()); + MyVarCleanupStack.register(metadataCell); + strong.set(new RuntimeScalar(1)); + assertTrue(MortalList.isUnblessedAggregateReachableDuringDrain(child)); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + metadataCell.set(new RuntimeScalar()); + + assertEquals(RuntimeScalarType.UNDEF, weak.type); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, child.perlLifecycleState()); + ReachabilityQueryStats stats = measurement.stats(); + assertEquals(0, stats.rootQueries); + assertEquals(0, stats.snapshotsBuilt); + assertEquals(0, stats.externalRootSnapshotsBuilt); + assertEquals(0, stats.liveRootSnapshotsBuilt); + assertEquals(0, stats.scalarReachabilityQueries); + } + } finally { + MyVarCleanupStack.popMark(lexicalMark); + runtime.lifecycleState.weakSweepSuppressionDepth--; + } + } + } + + @Test + void voidDiscardBorrowsAnActiveCapturedScalarInsteadOfConsumingItsOwner() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray target = new RuntimeArray(); + RuntimeScalar pad = new RuntimeScalar(); + pad.set(target.createAnonymousReference()); + PerlOwnerSlot capture = pad.retainClosureCaptureSlot(); + PerlOwnerSlot valueOwner = pad.countedOwnerSlot(); + RuntimeList result = new RuntimeList(); + result.elements.add(pad); + + MortalList.mortalizeForVoidDiscard(result); + + assertTrue(pad.refCountOwned); + assertTrue(valueOwner.isActive()); + assertEquals(1, target.refCount); + assertTrue(runtime.lifecycleState.pendingOwnerReleases.isEmpty()); + pad.releaseClosureCaptureSlot(capture); + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarUncountedOwnerRepairTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarUncountedOwnerRepairTest.java new file mode 100644 index 0000000000..a07fc1587e --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarUncountedOwnerRepairTest.java @@ -0,0 +1,78 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class RuntimeScalarUncountedOwnerRepairTest { + @Test + void uncountedContainerSlotKeepsWeakReferentAliveWithoutRootWalk() { + Assumptions.assumeFalse(ReachabilityWalker.correctiveSweepsEnabled(), + "this lifecycle regression requires corrective sweeps to be disabled"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash target = new RuntimeHash(); + target.blessId = NameNormalizer.getBlessId( + "RuntimeScalarUncountedOwnerRepairTest::Target"); + target.refCount = -1; + + RuntimeArray ownerContainer = new RuntimeArray(); + RuntimeScalar uncountedOwner = referenceTo(target); + ownerContainer.elements.add(uncountedOwner); + PerlOwnerSlot ownerSlot = uncountedOwner.untrackedReferenceOwnerSlot(); + assertNotNull(ownerSlot); + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + RuntimeScalar countedOwner = new RuntimeScalar(); + countedOwner.set(target.createAnonymousReference()); + RuntimeScalar weakObserver = target.createAnonymousReference(); + WeakRefRegistry.weaken(weakObserver); + assertTrue(WeakRefRegistry.isweak(weakObserver)); + + try { + // Model a transient selective-count undercount. Releasing this + // last counted slot leaves the array element as the strong owner. + target.refCount = 1; + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + countedOwner.set(new RuntimeScalar()); + + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, target.refCount); + assertTrue(WeakRefRegistry.isweak(weakObserver)); + assertFalse(target.destroyFired); + assertEquals(0, measurement.stats().rootQueries, + "the uncounted slot replaces the package-root fallback"); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + uncountedOwner.set(new RuntimeScalar()); + + assertFalse(ownerSlot.isActive()); + assertFalse(WeakRefRegistry.isweak(weakObserver), + "the last uncounted owner release clears weak observers"); + assertEquals(RuntimeScalarType.UNDEF, weakObserver.type); + assertEquals(Integer.MIN_VALUE, target.refCount); + } finally { + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + if (ownerSlot.isActive()) uncountedOwner.set(new RuntimeScalar()); + countedOwner.set(new RuntimeScalar()); + } + } + } + + private static RuntimeScalar referenceTo(RuntimeBase referent) { + RuntimeScalar scalar = new RuntimeScalar(); + scalar.type = RuntimeScalarType.REFERENCE; + scalar.value = referent; + return scalar; + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarUntrackedOwnerIdentityTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarUntrackedOwnerIdentityTest.java new file mode 100644 index 0000000000..35b4f361d6 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarUntrackedOwnerIdentityTest.java @@ -0,0 +1,35 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Tag("unit") +class RuntimeScalarUntrackedOwnerIdentityTest { + @Test + void staleLegacyScalarCellCountDoesNotKeepUntrackedOwnerAlive() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + RuntimeHash referent = new RuntimeHash(); + referent.refCount = WeakRefRegistry.WEAKLY_TRACKED; + + RuntimeScalar cell = new RuntimeScalar(); + cell.type = RuntimeScalarType.HASHREFERENCE; + cell.value = referent; + cell.refCount = 1; + cell.referencedByScalarReference = true; + cell.refreshUntrackedReferenceOwner(); + + assertNotNull(cell.untrackedReferenceOwnerSlot(), + "legacy compatibility count initially allows the owner edge to be recorded"); + assertEquals(1, referent.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + assertFalse(referent.hasUntrackedReferenceOwnerSlotIdentity(), + "without a live scalar-cell identity, the legacy count is not proof of ownership"); + assertFalse(cell.untrackedReferenceOwnerSlot().isActive()); + assertEquals(0, referent.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/ScalarRegistryIsNotPerlOwnerTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/ScalarRegistryIsNotPerlOwnerTest.java new file mode 100644 index 0000000000..4fd976c546 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/ScalarRegistryIsNotPerlOwnerTest.java @@ -0,0 +1,41 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@Tag("unit") +class ScalarRegistryIsNotPerlOwnerTest { + @Test + void jvmLiveRegistryOnlyScalarDoesNotRescueZeroCountReferent() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + runtime.lifecycleState.weakRefsExist = true; + + RuntimeArray referent = new RuntimeArray(); + referent.refCount = 1; + + // This wrapper remains Java-live in the test, but it has no Perl + // storage slot, lexical binding, or owner identity. + RuntimeScalar registryOnly = new RuntimeScalar(); + registryOnly.type = RuntimeScalarType.REFERENCE; + registryOnly.value = referent; + ScalarRefRegistry.registerRef(registryOnly); + assertFalse(registryOnly.hasOwnerSlots()); + + RuntimeScalar observer = new RuntimeScalar(); + observer.type = RuntimeScalarType.REFERENCE; + observer.value = referent; + observer.refCountOwned = true; + WeakRefRegistry.weaken(observer); + + assertEquals(Integer.MIN_VALUE, referent.refCount, + "a JVM registry entry without a Perl owner cannot rescue the referent"); + assertEquals(RuntimeScalarType.UNDEF, observer.type, + "the last real owner release clears the weak observer immediately"); + assertFalse(WeakRefRegistry.hasWeakRefsTo(referent)); + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/TailCallArgumentOwnerSlotFastPathTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/TailCallArgumentOwnerSlotFastPathTest.java new file mode 100644 index 0000000000..2874ced63e --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/TailCallArgumentOwnerSlotFastPathTest.java @@ -0,0 +1,48 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class TailCallArgumentOwnerSlotFastPathTest { + @Test + void survivingElementOwnerRepairsUndercountedTailCallWithoutRootWalk() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash target = new RuntimeHash(); + target.blessId = NameNormalizer.getBlessId("Issue1649::TailCallOwnerSlot"); + RuntimeArray backing = new RuntimeArray(); + backing.addClonedElement(target.createAnonymousReference()); + + RuntimeScalar tailArgument = new RuntimeScalar(); + tailArgument.set(target.createAnonymousReference()); + RuntimeArray tailArguments = new RuntimeArray(1); + tailArguments.elements.add(tailArgument); + tailArguments.elementsOwned = true; + + RuntimeScalar weak = target.createAnonymousReference(); + WeakRefRegistry.weaken(weak); + try { + assertEquals(2, target.countedOwnerSlotCount()); + target.refCount = 1; // Model a transient legacy-count underflow. + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + MortalList.releaseTailCallArgs(tailArguments); + + assertEquals(1, target.refCount); + assertEquals(1, target.countedOwnerSlotCount()); + assertTrue(WeakRefRegistry.isweak(weak)); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } finally { + WeakRefRegistry.unweaken(weak); + backing.undefine(); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/TailCallNoRootFallbackTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/TailCallNoRootFallbackTest.java new file mode 100644 index 0000000000..d9ce59cfd3 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/TailCallNoRootFallbackTest.java @@ -0,0 +1,39 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class TailCallNoRootFallbackTest { + @Test + void javaLiveScalarRegistryDoesNotRescueTailCallReferentOrWalkRoots() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar owner = new RuntimeScalar(); + owner.set(referent.createAnonymousReference()); + + RuntimeScalar weak = referent.createReference(); + WeakRefRegistry.weaken(weak); + ScalarRefRegistry.registerRef(owner); + RuntimeArray tailArguments = new RuntimeArray(1); + tailArguments.elements.add(owner); + tailArguments.elementsOwned = true; + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + MortalList.releaseTailCallArgs(tailArguments); + + assertFalse(owner.refCountOwned); + assertTrue(ScalarRefRegistry.snapshot().contains(owner)); + assertEquals(Integer.MIN_VALUE, referent.refCount); + assertEquals(RuntimeScalarType.UNDEF, weak.type); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/TailCallScalarRegistryOwnershipTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/TailCallScalarRegistryOwnershipTest.java new file mode 100644 index 0000000000..f2aebf9782 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/TailCallScalarRegistryOwnershipTest.java @@ -0,0 +1,37 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class TailCallScalarRegistryOwnershipTest { + @Test + void javaLiveScalarRegistryDoesNotRescueReleasedTailCallReferent() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + RuntimeHash referent = new RuntimeHash(); + RuntimeScalar owner = new RuntimeScalar(); + owner.set(referent.createAnonymousReference()); + + RuntimeScalar weak = referent.createReference(); + WeakRefRegistry.weaken(weak); + ScalarRefRegistry.registerRef(owner); + assertTrue(ScalarRefRegistry.snapshot().contains(owner)); + assertTrue(WeakRefRegistry.hasWeakRefsTo(referent)); + + RuntimeArray tailArguments = new RuntimeArray(1); + tailArguments.elements.add(owner); + tailArguments.elementsOwned = true; + MortalList.releaseTailCallArgs(tailArguments); + + assertFalse(owner.refCountOwned); + assertTrue(ScalarRefRegistry.snapshot().contains(owner), + "the Java-live scalar remains registered after its Perl owner leaves"); + assertEquals(Integer.MIN_VALUE, referent.refCount); + assertEquals(RuntimeScalarType.UNDEF, weak.type); + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/TailCallUncountedOwnerSlotFastPathTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/TailCallUncountedOwnerSlotFastPathTest.java new file mode 100644 index 0000000000..287587eccf --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/TailCallUncountedOwnerSlotFastPathTest.java @@ -0,0 +1,55 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class TailCallUncountedOwnerSlotFastPathTest { + @Test + void packageScalarOwnerDefersFinalTailCallReleaseWithoutRootWalk() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar target = new RuntimeScalar(); + String globalName = + "TailCallUncountedOwnerSlotFastPathTest::packageScalarOwner"; + GlobalVariable.globalVariables.put(globalName, target); + target.refCount = 0; + + RuntimeScalar tailArgument = new RuntimeScalar(); + tailArgument.set(target.createReference()); + RuntimeArray tailArguments = new RuntimeArray(1); + tailArguments.elements.add(tailArgument); + tailArguments.elementsOwned = true; + + RuntimeScalar weak = target.createReference(); + WeakRefRegistry.weaken(weak); + + try { + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_SCALAR_SLOT)); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + MortalList.releaseTailCallArgs(tailArguments); + + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, target.refCount); + assertTrue(WeakRefRegistry.isweak(weak)); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + GlobalVariable.globalVariables.remove(globalName); + + assertFalse(WeakRefRegistry.isweak(weak)); + assertEquals(RuntimeScalarType.UNDEF, weak.type); + assertEquals(Integer.MIN_VALUE, target.refCount); + } finally { + if (WeakRefRegistry.isweak(weak)) WeakRefRegistry.unweaken(weak); + GlobalVariable.globalVariables.remove(globalName); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/TargetedWeakReleaseOwnerSlotFastPathTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/TargetedWeakReleaseOwnerSlotFastPathTest.java new file mode 100644 index 0000000000..d5fa486cce --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/TargetedWeakReleaseOwnerSlotFastPathTest.java @@ -0,0 +1,49 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class TargetedWeakReleaseOwnerSlotFastPathTest { + @Test + void activeElementOwnerAvoidsTargetedRootWalk() { + Assumptions.assumeTrue(ReachabilityWalker.correctiveSweepsEnabled(), + "this compatibility path only runs when corrective sweeps are enabled"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray owner = new RuntimeArray(); + RuntimeArray target = new RuntimeArray(); + owner.addClonedElement(target.createAnonymousReference()); + RuntimeScalar weak = target.createAnonymousReference(); + WeakRefRegistry.weaken(weak); + + try { + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.ARRAY_ELEMENT)); + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + ReachabilityWalker.ReleasedWeakSweepResult result = + ReachabilityWalker.sweepReleasedWeakReferents( + Collections.singleton(target)); + + assertNull(result.live(), + "a slot owner needs no reachability snapshot"); + assertTrue(result.countedStrongOwners().contains(target)); + assertTrue(WeakRefRegistry.isweak(weak), + "the active element slot keeps the target's weak observer intact"); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } finally { + WeakRefRegistry.unweaken(weak); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/TemporaryReturnOwnerTraversalTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/TemporaryReturnOwnerTraversalTest.java new file mode 100644 index 0000000000..bebda961f6 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/TemporaryReturnOwnerTraversalTest.java @@ -0,0 +1,53 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.AbstractMap; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class TemporaryReturnOwnerTraversalTest { + @Test + void lexicalCleanupDoesNotWalkUnrelatedTemporaryReferentGraphs() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash metadata = new RuntimeHash(); + metadata.elements = new AbstractMap<>() { + @Override + public Set> entrySet() { + throw new AssertionError("lexical cleanup walked an unrelated referent graph"); + } + }; + RuntimeScalar expression = metadata.createAnonymousReference(); + RuntimeArray local = new RuntimeArray(); + local.localBindingExists = true; + MortalList.pushTemporaryRoot(expression); + try { + assertDoesNotThrow(() -> MortalList.scopeExitCleanupArray(local)); + assertFalse(local.localBindingExists); + } finally { + MortalList.popTemporaryRoot(expression); + } + } + } + + @Test + void expressionListsStillBorrowTheirDirectAggregateReferents() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray target = new RuntimeArray(); + RuntimeList expression = new RuntimeList(); + expression.elements.add(target.createReference()); + MortalList.pushTemporaryRoot(expression); + try { + assertTrue(MortalList.temporaryRootDirectlyReferences(target)); + } finally { + MortalList.popTemporaryRoot(expression); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/TiedHandlerOwnerTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/TiedHandlerOwnerTest.java new file mode 100644 index 0000000000..b2106ea10d --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/TiedHandlerOwnerTest.java @@ -0,0 +1,121 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class TiedHandlerOwnerTest { + @Test + void wrappersHaveDistinctOwnersAndDoNotBorrowTheirSourceCell() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash handler = new RuntimeHash(); + handler.refCount = 0; + RuntimeScalar source = handler.createAnonymousReference(); + TieHash hash = new TieHash("Handler", new RuntimeHash(), source); + TieArray array = new TieArray("Handler", new RuntimeArray(), source, new RuntimeArray()); + TieScalar scalar = new TieScalar("Handler", new RuntimeScalar(), source); + TieHandle handle = new TieHandle("Handler", new RuntimeIO(), source); + assertEquals(4, handler.ownerSlotCount(PerlOwnerSlot.Kind.TIE_HANDLER)); + assertEquals(4, handler.ownerSlotSnapshot().stream() + .map(PerlOwnerSlot::identity).distinct().count()); + assertEquals(4, handler.refCount); + source.set(RuntimeScalarCache.scalarUndef); + assertSame(handler, hash.getSelf().value); + assertSame(handler, array.getSelf().value); + assertSame(handler, scalar.getSelf().value); + RuntimeScalar weak = new RuntimeScalar(); + weak.set(handler.createAnonymousReference()); + WeakRefRegistry.weaken(weak); + hash.releaseTiedObject(); + assertSame(handler, hash.getSelf().value, + "retired magic can still borrow its snapshot during local restoration"); + assertFalse(hash.getSelf().refCountOwned); + hash.releaseTiedObject(); + array.releaseTiedObject(); + array.releaseTiedObject(); + assertEquals(2, handler.refCount); + assertEquals(2, handler.ownerSlotCount(PerlOwnerSlot.Kind.TIE_HANDLER)); + scalar.releaseTiedObject(); + scalar.releaseTiedObject(); + assertEquals(RuntimeScalarType.HASHREFERENCE, weak.type); + try (MortalList.ReachabilityQueryMeasurement measurement = MortalList.measureReachabilityQueries()) { + handle.releaseTiedObject(); + handle.releaseTiedObject(); + assertEquals(RuntimeScalarType.UNDEF, weak.type); + assertFalse(handler.hasOwnerSlots()); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, handler.perlLifecycleState()); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } + } + + @Test + void finalHashDestructionReleasesMagicDespiteAnExpressionBorrow() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash handler = new RuntimeHash(); + handler.refCount = 0; + RuntimeHash outer = new RuntimeHash(); + outer.type = RuntimeHash.TIED_HASH; + outer.elements = new TieHash("Handler", new RuntimeHash(), handler.createAnonymousReference()); + RuntimeScalar weak = new RuntimeScalar(); + weak.set(handler.createAnonymousReference()); + WeakRefRegistry.weaken(weak); + RuntimeScalar borrowed = outer.createAnonymousReference(); + MortalList.pushTemporaryRoot(borrowed); + try { + outer.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroyFromCountedOwner(outer); + assertEquals(RuntimeScalarType.UNDEF, weak.type); + assertFalse(handler.hasOwnerSlots()); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, outer.perlLifecycleState()); + } finally { + MortalList.popTemporaryRoot(borrowed); + } + } + } + @Test + void finalContainerReleaseDropsItsChildSlotsDespiteExpressionBorrows() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash hashChild = new RuntimeHash(); + RuntimeHash hash = new RuntimeHash(); + hash.put("child", hashChild.createAnonymousReference()); + RuntimeScalar hashWeak = new RuntimeScalar(); + hashWeak.set(hashChild.createAnonymousReference()); + WeakRefRegistry.weaken(hashWeak); + RuntimeHash arrayChild = new RuntimeHash(); + RuntimeArray array = new RuntimeArray(); + array.push(arrayChild.createAnonymousReference()); + RuntimeScalar arrayWeak = new RuntimeScalar(); + arrayWeak.set(arrayChild.createAnonymousReference()); + WeakRefRegistry.weaken(arrayWeak); + RuntimeScalar borrowedHash = hash.createAnonymousReference(); + RuntimeScalar borrowedArray = array.createAnonymousReference(); + MortalList.pushTemporaryRoot(borrowedHash); + MortalList.pushTemporaryRoot(borrowedArray); + try (MortalList.ReachabilityQueryMeasurement measurement = MortalList.measureReachabilityQueries()) { + hash.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroyFromCountedOwner(hash); + array.refCount = Integer.MIN_VALUE; + DestroyDispatch.callDestroyFromCountedOwner(array); + // Container finalization queues child decrements; the boundary + // drains those identified releases before observers are checked. + MortalList.flush(); + assertEquals(RuntimeScalarType.UNDEF, hashWeak.type); + assertEquals(RuntimeScalarType.UNDEF, arrayWeak.type); + assertFalse(hashChild.hasOwnerSlots()); + assertFalse(arrayChild.hasOwnerSlots()); + assertEquals(0, measurement.stats().rootQueries); + } finally { + MortalList.popTemporaryRoot(borrowedArray); + MortalList.popTemporaryRoot(borrowedHash); + } + } + } + +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/UnblessedScalarCellOwnerSlotRepairTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/UnblessedScalarCellOwnerSlotRepairTest.java new file mode 100644 index 0000000000..16e9ab20eb --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/UnblessedScalarCellOwnerSlotRepairTest.java @@ -0,0 +1,189 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class UnblessedScalarCellOwnerSlotRepairTest { + @Test + void unattachedScalarWrapperDoesNotCountAsCurrentPerlCellOwner() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash target = new RuntimeHash(); + RuntimeScalar unrootedOwner = new RuntimeScalar(); + unrootedOwner.set(target.createAnonymousReference()); + try { + assertFalse(unrootedOwner.isCurrentCountedScalarOwner( + target, unrootedOwner.countedOwnerSlot()), + "a Java-reachable wrapper without Perl storage is not a current owner"); + } finally { + unrootedOwner.set(new RuntimeScalar()); + } + } + } + + @Test + void captureCountWithoutLiveCodeOwnerDoesNotKeepScalarCellCurrent() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash target = new RuntimeHash(); + RuntimeScalar capturedOwner = new RuntimeScalar(); + capturedOwner.set(target.createAnonymousReference()); + capturedOwner.createReference(); + RuntimeCode closure = new RuntimeCode((args, context) -> new RuntimeList(), null); + closure.capturedScalars = new RuntimeScalar[]{capturedOwner}; + closure.capturedScalarOwnerSlots = new PerlOwnerSlot[]{ + capturedOwner.retainClosureCaptureSlot(closure) + }; + try { + assertFalse(closure.hasPerlOwnerForCapture()); + assertFalse(capturedOwner.isCurrentCountedScalarOwner( + target, capturedOwner.countedOwnerSlot()), + "capture metadata alone does not prove a live closure owner"); + } finally { + closure.releaseCaptures(); + capturedOwner.set(new RuntimeScalar()); + } + } + } + + @Test + void codeSlotCountsAsCurrentScalarBackedOwner() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeCode code = new RuntimeCode((args, context) -> new RuntimeList(), null); + RuntimeScalar codeOwner = new RuntimeScalar(code); + codeOwner.retainCodeSlotBinding(); + try { + assertEquals(PerlOwnerSlot.Kind.CODE_SLOT, codeOwner.countedOwnerSlot().kind()); + assertTrue(codeOwner.isCurrentCountedScalarOwner(code, + codeOwner.countedOwnerSlot())); + } finally { + codeOwner.releaseCodeSlotBinding(); + } + } + } + + @Test + void scalarOverwriteRestoresFromSurvivingScalarCellWithoutRootWalk() { + exerciseScalarCellRepair(false); + } + + @Test + void deferredReleaseRestoresFromSurvivingScalarCellWithoutRootWalk() { + exerciseScalarCellRepair(true); + } + + @Test + void scalarAndAggregateOwnersAreCountedTogetherAfterUndercount() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + boolean previousMortalListState = MortalList.isActive(); + MortalList.setActive(true); + RuntimeHash target = new RuntimeHash(); + RuntimeScalar firstOwner = new RuntimeScalar(); + RuntimeScalar survivingScalarOwner = new RuntimeScalar(); + RuntimeArray aggregateOwner = new RuntimeArray(); + RuntimeScalar weakObserver = target.createAnonymousReference(); + int mark = MyVarCleanupStack.pushMark(); + try { + MyVarCleanupStack.register(firstOwner); + MyVarCleanupStack.register(survivingScalarOwner); + firstOwner.set(target.createAnonymousReference()); + survivingScalarOwner.set(target.createAnonymousReference()); + aggregateOwner.push(target.createAnonymousReference()); + WeakRefRegistry.weaken(weakObserver); + + assertEquals(3, target.refCount); + target.refCount = 1; // Model one undercount across mixed owner families. + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + firstOwner.set(new RuntimeScalar()); + + assertEquals(2, target.refCount); + assertTrue(WeakRefRegistry.isweak(weakObserver)); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + survivingScalarOwner.set(new RuntimeScalar()); + RuntimeArray.pop(aggregateOwner); + MortalList.flush(); + assertFalse(WeakRefRegistry.isweak(weakObserver), + "the last scalar and aggregate owners clear the weak observer"); + } finally { + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + firstOwner.set(new RuntimeScalar()); + survivingScalarOwner.set(new RuntimeScalar()); + while (!aggregateOwner.elements.isEmpty()) RuntimeArray.pop(aggregateOwner); + MortalList.flush(); + MyVarCleanupStack.unregister(firstOwner); + MyVarCleanupStack.unregister(survivingScalarOwner); + MyVarCleanupStack.popMark(mark); + MortalList.setActive(previousMortalListState); + } + } + } + + private void exerciseScalarCellRepair(boolean deferRelease) { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + boolean previousMortalListState = MortalList.isActive(); + MortalList.setActive(true); + RuntimeHash target = new RuntimeHash(); + RuntimeScalar firstOwner = new RuntimeScalar(); + RuntimeScalar survivingOwner = new RuntimeScalar(); + RuntimeScalar weakObserver = target.createAnonymousReference(); + int mark = MyVarCleanupStack.pushMark(); + try { + MyVarCleanupStack.register(firstOwner); + MyVarCleanupStack.register(survivingOwner); + firstOwner.set(target.createAnonymousReference()); + survivingOwner.set(target.createAnonymousReference()); + WeakRefRegistry.weaken(weakObserver); + + assertEquals(2, target.refCount); + assertEquals(2, target.ownerSlotCount(PerlOwnerSlot.Kind.SCALAR_CELL)); + target.refCount = 1; // Model a transient legacy-count undercount. + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + if (deferRelease) { + MortalList.pushMark(); + MortalList.deferDecrement(target); + MortalList.popAndFlush(); + } else { + firstOwner.set(new RuntimeScalar()); + } + + assertEquals(deferRelease ? 2 : 1, target.refCount); + assertTrue(WeakRefRegistry.isweak(weakObserver)); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + firstOwner.set(new RuntimeScalar()); + survivingOwner.set(new RuntimeScalar()); + assertFalse(WeakRefRegistry.isweak(weakObserver), + "the final scalar-cell owner release clears the weak observer"); + } finally { + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + firstOwner.set(new RuntimeScalar()); + survivingOwner.set(new RuntimeScalar()); + MyVarCleanupStack.unregister(firstOwner); + MyVarCleanupStack.unregister(survivingOwner); + MyVarCleanupStack.popMark(mark); + MortalList.setActive(previousMortalListState); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/UncountedOwnerSlotZeroCountReleaseTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/UncountedOwnerSlotZeroCountReleaseTest.java new file mode 100644 index 0000000000..aaaf813bcf --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/UncountedOwnerSlotZeroCountReleaseTest.java @@ -0,0 +1,177 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Tag("unit") +class UncountedOwnerSlotZeroCountReleaseTest { + @Test + void scalarOverwriteDefersFinalCleanupUntilUncountedOwnerLeaves() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + Fixture fixture = fixture(); + assertFinalOwnerRelease(fixture, + () -> fixture.countedOwner.set(new RuntimeScalar())); + } + } + + @Test + void deferredDecrementDefersFinalCleanupUntilUncountedOwnerLeaves() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + Fixture fixture = fixture(); + assertFinalOwnerRelease(fixture, () -> { + MortalList.deferDecrementIfTracked(fixture.countedOwner); + MortalList.flushAboveMark(); + }); + } + } + + @Test + void tailCallReleaseDefersFinalCleanupUntilUncountedOwnerLeaves() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + Fixture fixture = fixture(); + RuntimeArray tailArguments = new RuntimeArray(1); + tailArguments.elements.add(fixture.countedOwner); + tailArguments.elementsOwned = true; + assertFinalOwnerRelease(fixture, + () -> MortalList.releaseTailCallArgs(tailArguments)); + } + } + + @Test + void finalRawOwnerSlotReleaseFinishesDeferredDestruction() { + try (PerlRuntime.Binding ignored = new PerlRuntime().bind()) { + String className = "UncountedOwnerSlotZeroCountReleaseTest::RawTarget"; + String destroyFqn = className + "::DESTROY"; + RuntimeHash target = new RuntimeHash(); + target.blessId = NameNormalizer.getBlessId(className); + target.refCount = -1; + + AtomicInteger destroyCalls = new AtomicInteger(); + RuntimeCode destroy = new RuntimeCode((args, context) -> { + destroyCalls.incrementAndGet(); + return new RuntimeList(); + }, null); + destroy.packageName = "UncountedOwnerSlotZeroCountReleaseTest"; + destroy.subName = "DESTROY"; + GlobalVariable.getGlobalCodeRef(destroyFqn).set(new RuntimeScalar(destroy)); + DestroyDispatch.registerIfDestroyable(target, target.blessId); + + RuntimeScalar countedOwner = new RuntimeScalar(); + countedOwner.set(target.createAnonymousReference()); + target.refCount = 1; + PerlOwnerSlot rawOwner = new PerlOwnerSlot(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE); + rawOwner.acquireUntrackedIdentity(target); + try { + target.deferFinalReleaseToUncountedOwnerSlots(); + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, target.refCount); + + target.releaseActiveOwner(countedOwner); + countedOwner.refCountOwned = false; + assertEquals(0, destroyCalls.get(), + "the remaining raw owner slot keeps the referent alive"); + + rawOwner.release(); + + assertEquals(Integer.MIN_VALUE, target.refCount); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, + target.perlLifecycleState()); + assertEquals(1, destroyCalls.get(), + "the last owner identity dispatches destruction exactly once"); + } finally { + if (rawOwner.isActive()) rawOwner.release(); + if (countedOwner.countedOwnerSlot() != null + && countedOwner.countedOwnerSlot().isActive()) { + target.releaseActiveOwner(countedOwner); + } + GlobalVariable.globalCodeRefs.remove(destroyFqn); + DestroyDispatch.unregisterDestroyable(target); + } + } + } + + private static Fixture fixture() { + String className = "UncountedOwnerSlotZeroCountReleaseTest::Target"; + RuntimeHash target = new RuntimeHash(); + target.blessId = NameNormalizer.getBlessId(className); + target.refCount = -1; + + AtomicInteger destroyCalls = new AtomicInteger(); + RuntimeCode destroy = new RuntimeCode((args, context) -> { + destroyCalls.incrementAndGet(); + return new RuntimeList(); + }, null); + destroy.packageName = "UncountedOwnerSlotZeroCountReleaseTest"; + destroy.subName = "DESTROY"; + String destroyFqn = className + "::DESTROY"; + GlobalVariable.getGlobalCodeRef(destroyFqn).set(new RuntimeScalar(destroy)); + + RuntimeArray ownerContainer = new RuntimeArray(); + RuntimeScalar uncountedOwner = referenceTo(target); + ownerContainer.elements.add(uncountedOwner); + PerlOwnerSlot ownerSlot = uncountedOwner.untrackedReferenceOwnerSlot(); + assertNotNull(ownerSlot); + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + RuntimeScalar countedOwner = new RuntimeScalar(); + countedOwner.set(target.createAnonymousReference()); + assertEquals(1, target.countedOwnerSlotCount()); + target.refCount = 1; // Model a transient legacy-count undercount. + assertFalse(WeakRefRegistry.hasWeakRefsTo(target)); + return new Fixture(target, ownerContainer, uncountedOwner, ownerSlot, countedOwner, + destroyCalls, destroyFqn); + } + + private static void assertFinalOwnerRelease(Fixture fixture, Runnable countedRelease) { + try { + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + countedRelease.run(); + + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, fixture.target.refCount); + assertEquals(RuntimeBase.PerlLifecycleState.LIVE, + fixture.target.perlLifecycleState()); + assertEquals(1, + fixture.target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertEquals(0, measurement.stats().rootQueries, + "the surviving owner slot replaces root-graph rescue"); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + fixture.uncountedOwner.set(new RuntimeScalar()); + + assertFalse(fixture.ownerSlot.isActive()); + assertEquals(0, + fixture.target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertEquals(Integer.MIN_VALUE, fixture.target.refCount); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, + fixture.target.perlLifecycleState()); + assertEquals(1, fixture.destroyCalls.get(), + "the final uncounted owner invokes DESTROY exactly once"); + } finally { + if (fixture.ownerSlot.isActive()) { + fixture.uncountedOwner.set(new RuntimeScalar()); + } + fixture.countedOwner.set(new RuntimeScalar()); + GlobalVariable.globalCodeRefs.remove(fixture.destroyFqn); + } + } + + private static RuntimeScalar referenceTo(RuntimeBase referent) { + RuntimeScalar scalar = new RuntimeScalar(); + scalar.type = RuntimeScalarType.REFERENCE; + scalar.value = referent; + return scalar; + } + + private record Fixture(RuntimeHash target, RuntimeArray ownerContainer, + RuntimeScalar uncountedOwner, PerlOwnerSlot ownerSlot, + RuntimeScalar countedOwner, AtomicInteger destroyCalls, + String destroyFqn) { + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/UntrackedAggregateFirstWeakenFastPathTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/UntrackedAggregateFirstWeakenFastPathTest.java new file mode 100644 index 0000000000..11537803e2 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/UntrackedAggregateFirstWeakenFastPathTest.java @@ -0,0 +1,59 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class UntrackedAggregateFirstWeakenFastPathTest { + @Test + void temporaryAggregateKeepsSentinelUntilDurableSlotWithoutRootWalk() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray target = new RuntimeArray(); + target.refCount = -1; + RuntimeScalar weakObserver = referenceTo(target); + String key = "UntrackedAggregateFirstWeakenFastPathTest::target"; + + try { + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + WeakRefRegistry.weaken(weakObserver); + + assertTrue(WeakRefRegistry.isweak(weakObserver)); + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, target.refCount); + assertEquals(0, measurement.stats().rootQueries, + "an untracked temporary needs no root-graph fallback"); + assertEquals(0, measurement.stats().edgesInspected); + assertEquals(0, measurement.stats().nodesVisited); + } + + GlobalVariable.globalArrays.put(key, target); + assertEquals(1, + target.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT)); + GlobalVariable.globalArrays.remove(key); + + assertFalse(WeakRefRegistry.isweak(weakObserver), + "the durable package slot's final release clears the weak observer"); + assertEquals(RuntimeScalarType.UNDEF, weakObserver.type); + assertEquals(Integer.MIN_VALUE, target.refCount); + } finally { + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + GlobalVariable.globalArrays.remove(key); + MortalList.invalidateAllRootSnapshots(); + } + } + } + + private static RuntimeScalar referenceTo(RuntimeBase referent) { + RuntimeScalar scalar = new RuntimeScalar(); + scalar.type = RuntimeScalarType.REFERENCE; + scalar.value = referent; + return scalar; + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/UntrackedAggregateOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/UntrackedAggregateOwnerSlotTest.java new file mode 100644 index 0000000000..a0fc067379 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/UntrackedAggregateOwnerSlotTest.java @@ -0,0 +1,231 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class UntrackedAggregateOwnerSlotTest { + @Test + void scalarCellsOwnUntrackedAggregateValuesAndReleaseEachIdentityOnce() { + Assumptions.assumeFalse(ReachabilityWalker.correctiveSweepsEnabled(), + "this lifecycle regression requires corrective sweeps to be disabled"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray target = new RuntimeArray(); + RuntimeScalar firstOwner = referenceTo(target); + RuntimeScalar secondOwner = referenceTo(target); + RuntimeScalar weakObserver = referenceTo(target); + int mark = MyVarCleanupStack.pushMark(); + try { + MyVarCleanupStack.register(firstOwner); + MyVarCleanupStack.register(secondOwner); + + PerlOwnerSlot firstSlot = firstOwner.untrackedReferenceOwnerSlot(); + PerlOwnerSlot secondSlot = secondOwner.untrackedReferenceOwnerSlot(); + assertNotNull(firstSlot); + assertNotNull(secondSlot); + assertNotSame(firstSlot, secondSlot, + "each durable scalar cell contributes its own owner identity"); + assertEquals(2, + target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertEquals(-1, target.refCount, + "explicit owner slots must not promote the legacy count"); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + WeakRefRegistry.weaken(weakObserver); + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, target.refCount); + assertTrue(WeakRefRegistry.isweak(weakObserver)); + assertEquals(0, measurement.stats().rootQueries, + "surviving owner identities avoid a root walk at first weaken"); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + firstOwner.set(new RuntimeScalar()); + assertFalse(firstSlot.isActive()); + assertTrue(secondSlot.isActive()); + assertEquals(1, + target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertTrue(WeakRefRegistry.isweak(weakObserver), + "one remaining scalar owner keeps the weak observer alive"); + assertNotEquals(RuntimeBase.PerlLifecycleState.DESTROYED, + target.perlLifecycleState()); + + firstOwner.set(new RuntimeScalar()); + assertEquals(1, + target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE), + "replacing an already-cleared cell must not release another identity"); + + secondOwner.set(new RuntimeScalar()); + assertFalse(secondSlot.isActive()); + assertEquals(0, + target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertFalse(WeakRefRegistry.isweak(weakObserver), + "the final owner release clears the weak observer immediately"); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, + target.perlLifecycleState(), + "the final owner release cascades aggregate cleanup"); + } finally { + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + RuntimeScalar.scopeExitCleanup(firstOwner); + RuntimeScalar.scopeExitCleanup(secondOwner); + MyVarCleanupStack.unregister(firstOwner); + MyVarCleanupStack.unregister(secondOwner); + MyVarCleanupStack.popMark(mark); + } + } + } + + @Test + void activeLexicalAggregateOwnerAvoidsTheFirstWeakRootWalk() { + Assumptions.assumeFalse(ReachabilityWalker.correctiveSweepsEnabled(), + "this lifecycle regression requires corrective sweeps to be disabled"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + MortalList.setActive(true); + RuntimeCode code = new RuntimeCode((args, context) -> new RuntimeList(), null); + RuntimeCode.pushActiveCode(code); + int mark = MyVarCleanupStack.pushMark(); + RuntimeArray target = new RuntimeArray(); + RuntimeScalar weakObserver = referenceTo(target); + try { + MyVarCleanupStack.register(target); + RuntimeCode.registerCurrentActiveLexical("@aggregate", target); + assertEquals(1, + target.ownerSlotCount(PerlOwnerSlot.Kind.ACTIVE_LEXICAL)); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + WeakRefRegistry.weaken(weakObserver); + assertTrue(WeakRefRegistry.isweak(weakObserver)); + assertEquals(0, measurement.stats().rootQueries, + "an active lexical owner identity replaces the root proof"); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + MortalList.scopeExitCleanupArray(target); + MyVarCleanupStack.unregister(target); + assertFalse(WeakRefRegistry.isweak(weakObserver)); + assertEquals(0, + target.ownerSlotCount(PerlOwnerSlot.Kind.ACTIVE_LEXICAL)); + } finally { + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + MyVarCleanupStack.unregister(target); + MyVarCleanupStack.popMark(mark); + RuntimeCode.popActiveCode(code); + } + } + } + + @Test + void packageAggregateOwnerAvoidsTheFirstWeakRootWalkAndFinalizesOnRemoval() { + Assumptions.assumeFalse(ReachabilityWalker.correctiveSweepsEnabled(), + "this lifecycle regression requires corrective sweeps to be disabled"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray target = new RuntimeArray(); + RuntimeScalar weakObserver = referenceTo(target); + String globalName = "UntrackedAggregateOwnerSlotTest::package_root"; + + try { + GlobalVariable.globalArrays.put(globalName, target); + assertEquals(1, + target.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT)); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + WeakRefRegistry.weaken(weakObserver); + assertTrue(WeakRefRegistry.isweak(weakObserver)); + assertEquals(0, measurement.stats().rootQueries, + "a package aggregate slot replaces the root proof"); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + GlobalVariable.globalArrays.remove(globalName); + assertEquals(0, + target.ownerSlotCount(PerlOwnerSlot.Kind.PACKAGE_AGGREGATE_SLOT)); + assertFalse(WeakRefRegistry.isweak(weakObserver)); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, + target.perlLifecycleState()); + } finally { + GlobalVariable.globalArrays.remove(globalName); + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + } + } + } + + @Test + void finalUntrackedBlessedAggregateOwnerRunsDestroyBeforeClearingWeakObserver() { + Assumptions.assumeFalse(ReachabilityWalker.correctiveSweepsEnabled(), + "this lifecycle regression requires corrective sweeps to be disabled"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + String className = "UntrackedAggregateOwnerSlotTest::BlessedArray"; + RuntimeHash target = new RuntimeHash(); + target.blessId = NameNormalizer.getBlessId(className); + RuntimeScalar owner = referenceTo(target); + RuntimeScalar weakObserver = referenceTo(target); + AtomicInteger destroyCalls = new AtomicInteger(); + AtomicBoolean weakDuringDestroy = new AtomicBoolean(); + int mark = MyVarCleanupStack.pushMark(); + RuntimeCode destroy = new RuntimeCode((args, context) -> { + destroyCalls.incrementAndGet(); + weakDuringDestroy.set(WeakRefRegistry.isweak(weakObserver)); + return new RuntimeList(); + }, null); + destroy.packageName = "UntrackedAggregateOwnerSlotTest"; + destroy.subName = "DESTROY"; + GlobalVariable.getGlobalCodeRef(className + "::DESTROY") + .set(new RuntimeScalar(destroy)); + + try { + MyVarCleanupStack.register(owner); + assertEquals(1, + target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + WeakRefRegistry.weaken(weakObserver); + assertTrue(WeakRefRegistry.isweak(weakObserver)); + + owner.set(new RuntimeScalar()); + + assertEquals(1, destroyCalls.get()); + assertTrue(weakDuringDestroy.get(), + "Perl keeps weak observers valid while DESTROY runs"); + assertFalse(WeakRefRegistry.isweak(weakObserver), + "weak observers clear after a non-resurrecting DESTROY"); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, + target.perlLifecycleState()); + } finally { + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + RuntimeScalar.scopeExitCleanup(owner); + MyVarCleanupStack.unregister(owner); + MyVarCleanupStack.popMark(mark); + GlobalVariable.removeGlobalCodeRefForStashDelete(className + "::DESTROY"); + } + } + } + + private static RuntimeScalar referenceTo(RuntimeBase referent) { + RuntimeScalar reference = new RuntimeScalar(); + reference.type = RuntimeScalarType.REFERENCE; + reference.value = referent; + return reference; + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/UntrackedReferenceOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/UntrackedReferenceOwnerSlotTest.java new file mode 100644 index 0000000000..619121e41f --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/UntrackedReferenceOwnerSlotTest.java @@ -0,0 +1,447 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class UntrackedReferenceOwnerSlotTest { + @Test + void untrackedElementOwnersReleaseAtAggregateScopeExitWithoutWeakRefs() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + MortalList.setActive(true); + RuntimeScalar arrayTarget = new RuntimeScalar("array target"); + RuntimeScalar arrayElement = referenceTo(arrayTarget); + RuntimeArray array = new RuntimeArray(); + array.addClonedElement(arrayElement); + PerlOwnerSlot arrayValueOwner = arrayElement.untrackedReferenceOwnerSlot(); + assertNotNull(arrayValueOwner); + + RuntimeScalar hashTarget = new RuntimeScalar("hash target"); + RuntimeScalar hashElement = referenceTo(hashTarget); + RuntimeHash hash = new RuntimeHash(); + hash.put("value", hashElement); + RuntimeScalar storedHashElement = hash.elements.get("value"); + PerlOwnerSlot hashValueOwner = storedHashElement.untrackedReferenceOwnerSlot(); + assertNotNull(hashValueOwner); + assertSame(hashTarget, hashValueOwner.referent()); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + MortalList.scopeExitCleanupArray(array); + MortalList.scopeExitCleanupHash(hash); + + assertFalse(arrayValueOwner.isActive()); + assertFalse(hashValueOwner.isActive()); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + assertEquals(0, measurement.stats().scalarReachabilityQueries); + + MortalList.scopeExitCleanupArray(array); + MortalList.scopeExitCleanupHash(hash); + assertEquals(0, arrayTarget.ownerSlotCount( + PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertEquals(0, hashTarget.ownerSlotCount( + PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + } + } + } + + @Test + void aggregateCollectionHooksRefreshUntrackedOwnersAfterInsertion() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + MortalList.setActive(true); + RuntimeScalar arrayTarget = new RuntimeScalar("raw array target"); + RuntimeScalar arrayElement = referenceTo(arrayTarget); + RuntimeArray array = new RuntimeArray(); + array.elements.add(arrayElement); + + PerlOwnerSlot arrayValueOwner = arrayElement.untrackedReferenceOwnerSlot(); + assertNotNull(arrayValueOwner); + assertSame(arrayTarget, arrayValueOwner.referent()); + assertEquals(-1, arrayTarget.refCount); + + RuntimeScalar hashTarget = new RuntimeScalar("raw hash target"); + RuntimeScalar hashElement = referenceTo(hashTarget); + RuntimeHash hash = new RuntimeHash(); + hash.elements.put("value", hashElement); + + PerlOwnerSlot hashValueOwner = hashElement.untrackedReferenceOwnerSlot(); + assertNotNull(hashValueOwner); + assertSame(hashTarget, hashValueOwner.referent()); + assertEquals(-1, hashTarget.refCount); + + array.elements.remove(arrayElement); + hash.elements.remove("value"); + MortalList.deferDestroyForContainerClear( + java.util.List.of(arrayElement, hashElement)); + assertFalse(arrayValueOwner.isActive()); + assertFalse(hashValueOwner.isActive()); + } + } + + @Test + void arrayCopyConstructionRefreshesOwnersAfterInstallingItsElementList() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar target = new RuntimeScalar("copied array target"); + RuntimeScalar element = referenceTo(target); + + RuntimeArray copy = new RuntimeArray(java.util.List.of(element)); + + PerlOwnerSlot owner = element.untrackedReferenceOwnerSlot(); + assertNotNull(owner); + assertSame(target, owner.referent()); + assertTrue(copy.elements.contains(element)); + assertEquals(-1, target.refCount); + } + } + + @Test + void untrackedContainerReferentsGetSlotIdentityWithoutCountPromotion() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + MortalList.setActive(true); + RuntimeScalar firstTarget = new RuntimeScalar("first"); + RuntimeScalar secondTarget = new RuntimeScalar("second"); + RuntimeArray targetStorage = new RuntimeArray(); + targetStorage.addClonedElement(firstTarget); + targetStorage.addClonedElement(secondTarget); + RuntimeArray array = new RuntimeArray(); + array.addClonedElement(firstTarget.createReference()); + + RuntimeScalar element = array.elements.get(0); + PerlOwnerSlot owner = element.untrackedReferenceOwnerSlot(); + assertNotNull(owner); + assertEquals(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE, owner.kind()); + assertSame(firstTarget, owner.referent()); + assertEquals(-1, firstTarget.refCount, + "slot tracking must not promote an arbitrary referent into the legacy count"); + + element.set(secondTarget.createReference()); + assertSame(owner, element.untrackedReferenceOwnerSlot()); + assertSame(secondTarget, owner.referent()); + assertFalse(firstTarget.hasOwnerSlots()); + assertEquals(-1, secondTarget.refCount); + + array.elements.remove(element); + MortalList.deferDestroyForContainerClear(java.util.List.of(element)); + assertFalse(owner.isActive()); + assertFalse(secondTarget.hasOwnerSlots()); + + RuntimeScalar weaklyTrackedTarget = new RuntimeScalar("weakly tracked"); + targetStorage.addClonedElement(weaklyTrackedTarget); + RuntimeScalar weakObserver = weaklyTrackedTarget.createReference(); + WeakRefRegistry.weaken(weakObserver); + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, weaklyTrackedTarget.refCount); + + RuntimeArray weaklyTrackedArray = new RuntimeArray(); + weaklyTrackedArray.addClonedElement(weaklyTrackedTarget.createReference()); + RuntimeScalar weaklyTrackedElement = weaklyTrackedArray.elements.get(0); + PerlOwnerSlot weaklyTrackedOwner = weaklyTrackedElement.untrackedReferenceOwnerSlot(); + assertNotNull(weaklyTrackedOwner); + assertSame(weaklyTrackedTarget, weaklyTrackedOwner.referent()); + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, weaklyTrackedTarget.refCount, + "a later strong store must not promote the legacy weakly-tracked sentinel"); + + WeakRefRegistry.unweaken(weakObserver); + weaklyTrackedArray.elements.remove(weaklyTrackedElement); + MortalList.deferDestroyForContainerClear(java.util.List.of(weaklyTrackedElement)); + assertFalse(weaklyTrackedOwner.isActive()); + } + } + + @Test + void untrackedLexicalValueSlotSurvivesCaptureAndReleasesAtFinalOwnerExit() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar target = new RuntimeScalar("weakly tracked target"); + RuntimeScalar weakObserver = new RuntimeScalar(); + weakObserver.type = RuntimeScalarType.REFERENCE; + weakObserver.value = target; + WeakRefRegistry.weaken(weakObserver); + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, target.refCount); + + RuntimeScalar lexical = new RuntimeScalar(); + int mark = MyVarCleanupStack.pushMark(); + MyVarCleanupStack.register(lexical); + + RuntimeScalar untrackedTarget = new RuntimeScalar("untracked lexical target"); + RuntimeScalar untrackedReference = new RuntimeScalar(); + untrackedReference.type = RuntimeScalarType.REFERENCE; + untrackedReference.value = untrackedTarget; + lexical.set(untrackedReference); + PerlOwnerSlot valueSlot = lexical.untrackedReferenceOwnerSlot(); + assertNotNull(valueSlot); + assertSame(untrackedTarget, valueSlot.referent()); + assertEquals(-1, untrackedTarget.refCount); + + RuntimeScalar targetReference = new RuntimeScalar(); + targetReference.type = RuntimeScalarType.REFERENCE; + targetReference.value = target; + lexical.set(targetReference); + assertSame(valueSlot, lexical.untrackedReferenceOwnerSlot()); + assertFalse(untrackedTarget.hasOwnerSlots()); + assertSame(target, valueSlot.referent()); + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, target.refCount, + "a lexical owner slot must not promote the weakly-tracked sentinel"); + + PerlOwnerSlot capture = lexical.retainClosureCaptureSlot(); + RuntimeScalar.scopeExitCleanup(lexical); + MyVarCleanupStack.unregister(lexical); + assertTrue(valueSlot.isActive(), "the closure must keep the lexical value slot alive"); + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + lexical.releaseClosureCaptureSlot(capture); + assertFalse(valueSlot.isActive(), "the final cell owner must retire the value slot"); + assertFalse(target.hasOwnerSlots()); + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, target.refCount); + MyVarCleanupStack.popMark(mark); + } + } + + @Test + void packageScalarAliasesOwnDistinctUntrackedReferenceSlotsUntilEachKeyLeaves() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar target = new RuntimeScalar("package-rooted referent"); + RuntimeScalar packageCell = new RuntimeScalar(); + packageCell.type = RuntimeScalarType.REFERENCE; + packageCell.value = target; + String firstName = "RuntimeScalarOwnerSlotTest::rooted_one"; + String secondName = "RuntimeScalarOwnerSlotTest::rooted_two"; + RuntimeScalar observer = new RuntimeScalar(); + observer.type = RuntimeScalarType.REFERENCE; + observer.value = target; + + try { + GlobalVariable.globalVariables.put(firstName, packageCell); + PerlOwnerSlot valueOwner = packageCell.untrackedReferenceOwnerSlot(); + assertNotNull(valueOwner, + "a package scalar cell owns its untracked reference value"); + assertEquals(-1, target.refCount, + "package ownership must not promote an untracked referent"); + assertEquals(1, packageCell.ownerSlotCount( + PerlOwnerSlot.Kind.PACKAGE_SCALAR_SLOT)); + + GlobalVariable.globalVariables.put(secondName, packageCell); + assertEquals(2, packageCell.ownerSlotCount( + PerlOwnerSlot.Kind.PACKAGE_SCALAR_SLOT), + "each package key has an independent scalar-cell owner identity"); + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + WeakRefRegistry.weaken(observer); + assertEquals(0, measurement.stats().rootQueries, + "explicit package owners avoid a root walk during weaken"); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + assertEquals(1, target.ownerSlotCount( + PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + + GlobalVariable.globalVariables.keySet().remove(firstName); + assertTrue(valueOwner.isActive(), + "removing one alias keeps the scalar's value owner alive"); + assertEquals(1, packageCell.ownerSlotCount( + PerlOwnerSlot.Kind.PACKAGE_SCALAR_SLOT)); + + GlobalVariable.globalVariables.remove(secondName); + assertFalse(valueOwner.isActive(), + "the final package key releases the untracked value edge"); + assertEquals(0, packageCell.ownerSlotCount( + PerlOwnerSlot.Kind.PACKAGE_SCALAR_SLOT)); + assertEquals(0, target.ownerSlotCount( + PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + } finally { + GlobalVariable.globalVariables.remove(firstName); + GlobalVariable.globalVariables.remove(secondName); + if (WeakRefRegistry.isweak(observer)) WeakRefRegistry.unweaken(observer); + } + } + } + + @Test + void clonedPackageScalarTableRebuildsOwnerIdentities() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar target = new RuntimeScalar("thread snapshot referent"); + RuntimeScalar packageCell = new RuntimeScalar(); + packageCell.type = RuntimeScalarType.REFERENCE; + packageCell.value = target; + runtime.globalState.scalarValues().put( + "RuntimeScalarOwnerSlotTest::snapshot_root", packageCell); + + runtime.globalState.rebuildPackageScalarOwnerSlots(); + + assertEquals(1, packageCell.ownerSlotCount( + PerlOwnerSlot.Kind.PACKAGE_SCALAR_SLOT)); + assertEquals(1, target.ownerSlotCount( + PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + assertEquals(-1, target.refCount, + "reconstructing a package root must not promote its untracked value"); + } + } + + @Test + void packageScalarSlotTracksDestroyedLegacyCellWithoutRevivingIt() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar legacyDestroyedCell = new RuntimeScalar("retired"); + legacyDestroyedCell.refCount = Integer.MIN_VALUE; + String key = "RuntimeScalarOwnerSlotTest::retired_cell"; + + try { + GlobalVariable.globalVariables.put(key, legacyDestroyedCell); + + assertEquals(Integer.MIN_VALUE, legacyDestroyedCell.refCount, + "identity tracking must not revive the legacy lifecycle count"); + assertEquals(1, legacyDestroyedCell.ownerSlotCount( + PerlOwnerSlot.Kind.PACKAGE_SCALAR_SLOT)); + + GlobalVariable.globalVariables.remove(key); + assertEquals(Integer.MIN_VALUE, legacyDestroyedCell.refCount); + assertEquals(0, legacyDestroyedCell.ownerSlotCount( + PerlOwnerSlot.Kind.PACKAGE_SCALAR_SLOT)); + } finally { + GlobalVariable.globalVariables.remove(key); + } + } + } + + @Test + void tiedHandlerOwnerTracksAndReleasesAnUntrackedValue() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar target = new RuntimeScalar("tied handler target"); + RuntimeScalar handlerReference = new RuntimeScalar(); + handlerReference.type = RuntimeScalarType.REFERENCE; + handlerReference.value = target; + RuntimeScalar handlerOwner = RuntimeScalar.retainTiedHandlerValue(handlerReference); + PerlOwnerSlot valueOwner = handlerOwner.untrackedReferenceOwnerSlot(); + RuntimeScalar observer = new RuntimeScalar(); + observer.type = RuntimeScalarType.REFERENCE; + observer.value = target; + + assertNotNull(valueOwner); + assertSame(target, valueOwner.referent()); + assertEquals(-1, target.refCount, + "the tie handler slot must not promote the legacy referent count"); + + try { + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + WeakRefRegistry.weaken(observer); + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, target.refCount); + assertEquals(0, measurement.stats().rootQueries, + "the tied handler identity avoids a root walk during weaken"); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + RuntimeScalar.releaseTiedHandlerValue(handlerOwner); + assertFalse(valueOwner.isActive()); + assertEquals(0, target.ownerSlotCount( + PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + RuntimeScalar.releaseTiedHandlerValue(handlerOwner); + assertEquals(0, target.ownerSlotCount( + PerlOwnerSlot.Kind.UNTRACKED_REFERENCE), + "repeated release must not retire another owner's identity"); + } finally { + if (WeakRefRegistry.isweak(observer)) WeakRefRegistry.unweaken(observer); + } + } + } + + @Test + void stagedArgumentOwnerTracksAndReleasesAnUntrackedValue() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar target = new RuntimeScalar("staged argument target"); + RuntimeScalar reference = new RuntimeScalar(); + reference.type = RuntimeScalarType.REFERENCE; + reference.value = target; + RuntimeScalar stagedOwner = RuntimeScalar.retainStagedArgumentValue(reference); + PerlOwnerSlot valueOwner = stagedOwner.untrackedReferenceOwnerSlot(); + + assertNotNull(valueOwner); + assertSame(target, valueOwner.referent()); + assertEquals(-1, target.refCount, + "the staged owner must not promote the legacy referent count"); + + RuntimeScalar.releaseStagedArgumentValue(stagedOwner); + assertFalse(valueOwner.isActive()); + assertEquals(0, target.ownerSlotCount( + PerlOwnerSlot.Kind.UNTRACKED_REFERENCE)); + RuntimeScalar.releaseStagedArgumentValue(stagedOwner); + assertEquals(0, target.ownerSlotCount( + PerlOwnerSlot.Kind.UNTRACKED_REFERENCE), + "repeated release must be idempotent"); + } + } + + @Test + void explicitUntrackedValueOwnerAvoidsGlobalCorrectiveSweep() { + Assumptions.assumeTrue(ReachabilityWalker.correctiveSweepsEnabled(), + "this scheduling regression requires corrective sweeps to be enabled"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeScalar target = new RuntimeScalar("untracked referent"); + RuntimeScalar owner = new RuntimeScalar(); + int mark = MyVarCleanupStack.pushMark(); + MyVarCleanupStack.register(owner); + RuntimeScalar untrackedReference = new RuntimeScalar(); + untrackedReference.type = RuntimeScalarType.REFERENCE; + untrackedReference.value = target; + owner.set(untrackedReference); + PerlOwnerSlot ownerSlot = owner.untrackedReferenceOwnerSlot(); + assertNotNull(ownerSlot); + assertEquals(PerlOwnerSlot.Kind.UNTRACKED_REFERENCE, ownerSlot.kind()); + assertSame(target, ownerSlot.referent()); + assertEquals(-1, target.refCount, + "the durable slot identity must not promote the compatibility count"); + + RuntimeScalar observer = new RuntimeScalar(); + observer.type = RuntimeScalarType.REFERENCE; + observer.value = target; + try { + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + WeakRefRegistry.weaken(observer); + + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, target.refCount); + assertTrue(WeakRefRegistry.isweak(observer)); + assertFalse(runtime.lifecycleState.immediateWeakSweepRequested, + "the explicit owner slot avoids a registry-wide correction"); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + boolean flushWasSuppressed = MortalList.suppressFlush(true); + try { + owner.set(new RuntimeScalar()); + } finally { + MortalList.suppressFlush(flushWasSuppressed); + } + assertFalse(ownerSlot.isActive()); + assertTrue(runtime.lifecycleState.targetedWeakSweepReferents.contains(target), + "the final untracked owner schedules targeted cleanup"); + } finally { + if (WeakRefRegistry.isweak(observer)) WeakRefRegistry.unweaken(observer); + MyVarCleanupStack.unregister(owner); + MyVarCleanupStack.popMark(mark); + } + } + } + + private static RuntimeScalar referenceTo(RuntimeBase referent) { + RuntimeScalar reference = new RuntimeScalar(); + reference.type = RuntimeScalarType.REFERENCE; + reference.value = referent; + return reference; + } + +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/WeakBackrefOwnerSlotFastPathTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/WeakBackrefOwnerSlotFastPathTest.java new file mode 100644 index 0000000000..b7b34c4bf8 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/WeakBackrefOwnerSlotFastPathTest.java @@ -0,0 +1,50 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class WeakBackrefOwnerSlotFastPathTest { + @Test + void elementOwnerAvoidsWeakCodeCaptureGraphWalk() { + Assumptions.assumeTrue(ReachabilityWalker.correctiveSweepsEnabled(), + "this compatibility path only runs when corrective sweeps are enabled"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray capturedParent = new RuntimeArray(); + RuntimeArray target = new RuntimeArray(); + capturedParent.addClonedElement(target.createAnonymousReference()); + assertEquals(1, target.ownerSlotCount(PerlOwnerSlot.Kind.ARRAY_ELEMENT)); + + RuntimeCode weakBackrefCode = new RuntimeCode(null, List.of()); + weakBackrefCode.capturedAggregates = new RuntimeBase[] {capturedParent}; + weakBackrefCode.captureFieldsRecorded = true; + RuntimeScalar weakCode = new RuntimeScalar(weakBackrefCode); + WeakRefRegistry.weaken(weakCode); + + RuntimeScalar weakTarget = target.createAnonymousReference(); + WeakRefRegistry.weaken(weakTarget); + try { + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + ReachabilityWalker.sweepWeakRefs(true, false); + + assertTrue(WeakRefRegistry.isweak(weakTarget), + "the active element owner keeps the weakly observed target alive"); + assertEquals(0, measurement.stats().weakBackrefCaptureSnapshotsBuilt, + "an owner-slot proof avoids the weak CODE capture graph walk"); + } + } finally { + WeakRefRegistry.unweaken(weakTarget); + WeakRefRegistry.unweaken(weakCode); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/WeakCodeOwnerSlotNoReachabilityTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/WeakCodeOwnerSlotNoReachabilityTest.java new file mode 100644 index 0000000000..926ee4df35 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/WeakCodeOwnerSlotNoReachabilityTest.java @@ -0,0 +1,47 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class WeakCodeOwnerSlotNoReachabilityTest { + @Test + void liveLexicalCodeOwnerPreservesWeakObserverWithoutRootWalk() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeCode code = new RuntimeCode((args, context) -> new RuntimeList(), null); + code.refCount = 0; + RuntimeScalar strongOwner = new RuntimeScalar(); + RuntimeScalar weakOwner = new RuntimeScalar(); + int mark = MyVarCleanupStack.pushMark(); + MyVarCleanupStack.register(strongOwner); + MyVarCleanupStack.register(weakOwner); + strongOwner.set(new RuntimeScalar(code)); + weakOwner.set(new RuntimeScalar(code)); + + try { + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + WeakRefRegistry.weaken(weakOwner); + + assertTrue(WeakRefRegistry.isweak(weakOwner)); + assertEquals(RuntimeScalarType.CODE, weakOwner.type); + assertEquals(0, measurement.stats().scalarReachabilityQueries, + "the remaining lexical owner slot is the liveness proof"); + assertEquals(0, measurement.stats().rootQueries); + } + + strongOwner.set(new RuntimeScalar()); + assertEquals(RuntimeScalarType.UNDEF, weakOwner.type, + "releasing the final lexical CODE owner clears its weak observer"); + } finally { + MyVarCleanupStack.unregister(weakOwner); + MyVarCleanupStack.unregister(strongOwner); + MyVarCleanupStack.popMark(mark); + } + } + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/WeakCodeOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/WeakCodeOwnerSlotTest.java new file mode 100644 index 0000000000..904ce45697 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/WeakCodeOwnerSlotTest.java @@ -0,0 +1,82 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class WeakCodeOwnerSlotTest { + @Test + void packageArrayCodeOwnerSlotSkipsReachabilityFallbacks() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeCode code = new RuntimeCode((args, context) -> new RuntimeList(), null); + RuntimeScalar strongCodeRef = new RuntimeScalar(code); + RuntimeArray callbacks = new RuntimeArray(); + callbacks.addClonedElement(strongCodeRef); + String globalName = "WeakCodeOwnerSlotTest::callbacks"; + GlobalVariable.globalArrays.put(globalName, callbacks); + + RuntimeScalar weakCodeRef = new RuntimeScalar(code); + WeakRefRegistry.weaken(weakCodeRef); + try { + assertTrue(code.hasOwnerSlots()); + assertTrue(code.ownerSlotSnapshot().stream().anyMatch( + slot -> slot.isActive() + && slot.kind() != PerlOwnerSlot.Kind.CODE_SLOT), + "package arrays create a direct owner identity for their CODE values"); + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + WeakRefRegistry.clearWeakRefsTo(code); + + assertTrue(WeakRefRegistry.isweak(weakCodeRef)); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + assertEquals(0, measurement.stats().scalarReachabilityQueries); + } + } finally { + GlobalVariable.globalArrays.remove(globalName); + callbacks.elements.remove(strongCodeRef); + MortalList.deferDestroyForContainerClear(java.util.List.of(strongCodeRef)); + if (WeakRefRegistry.isweak(weakCodeRef)) { + WeakRefRegistry.unweaken(weakCodeRef); + } + } + } + } + + @Test + void liveLexicalOwnerSlotSkipsCodeScalarGraphScan() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeCode code = new RuntimeCode((args, context) -> new RuntimeList(), null); + code.refCount = 0; + RuntimeScalar owner = new RuntimeScalar(); + int mark = MyVarCleanupStack.pushMark(); + MyVarCleanupStack.register(owner); + owner.set(new RuntimeScalar(code)); + RuntimeScalar observer = new RuntimeScalar(code); + WeakRefRegistry.weaken(observer); + + try { + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + WeakRefRegistry.clearWeakRefsTo(code); + + assertTrue(WeakRefRegistry.isweak(observer)); + assertEquals(0, measurement.stats().scalarReachabilityQueries, + "the live lexical owner identity avoids the scalar-root scan"); + assertEquals(0, measurement.stats().rootQueries); + } + } finally { + owner.set(new RuntimeScalar()); + MyVarCleanupStack.unregister(owner); + MyVarCleanupStack.popMark(mark); + } + assertEquals(RuntimeScalarType.UNDEF, observer.type, + "the final lexical owner release clears the weak CODE observer"); + } + } + +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/WeakUncountedOwnerSlotFastPathTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/WeakUncountedOwnerSlotFastPathTest.java new file mode 100644 index 0000000000..7d4c90c4a5 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/WeakUncountedOwnerSlotFastPathTest.java @@ -0,0 +1,72 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class WeakUncountedOwnerSlotFastPathTest { + @Test + void finalUncountedOwnerUsesSlotIdentityWithoutRootWalk() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + MortalList.setActive(true); + RuntimeArray target = new RuntimeArray(); + target.refCount = -1; + + RuntimeArray lexicalOwner = new RuntimeArray(); + int mark = MyVarCleanupStack.pushMark(); + MyVarCleanupStack.register(lexicalOwner); + RuntimeScalar strong = referenceTo(target); + lexicalOwner.elements.add(strong); + PerlOwnerSlot strongSlot = strong.untrackedReferenceOwnerSlot(); + assertNotNull(strongSlot); + + RuntimeScalar weakObserver = referenceTo(target); + // Model the counted slot being weakened; the array element remains + // the only uncounted strong Perl owner of the target. + target.refCount = 1; + weakObserver.refCountOwned = true; + try { + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + WeakRefRegistry.weaken(weakObserver); + + assertTrue(WeakRefRegistry.isweak(weakObserver)); + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, target.refCount, + "an uncounted slot retains the target without a compatibility pin"); + assertEquals(0, measurement.stats().rootQueries, + "the direct owner identity avoids a graph query"); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + + strong.set(new RuntimeScalar()); + + assertFalse(strongSlot.isActive()); + assertFalse(WeakRefRegistry.isweak(weakObserver), + "the final uncounted owner release clears the weak observer"); + assertEquals(RuntimeScalarType.UNDEF, weakObserver.type); + assertEquals(Integer.MIN_VALUE, target.refCount); + } finally { + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + if (strongSlot.isActive()) strong.set(new RuntimeScalar()); + MortalList.scopeExitCleanupArray(lexicalOwner); + MyVarCleanupStack.unregister(lexicalOwner); + MyVarCleanupStack.popMark(mark); + } + } + } + + private static RuntimeScalar referenceTo(RuntimeBase referent) { + RuntimeScalar scalar = new RuntimeScalar(); + scalar.type = RuntimeScalarType.REFERENCE; + scalar.value = referent; + return scalar; + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/WeakeningCountedOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/WeakeningCountedOwnerSlotTest.java new file mode 100644 index 0000000000..0eeb8a9cf0 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/WeakeningCountedOwnerSlotTest.java @@ -0,0 +1,55 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class WeakeningCountedOwnerSlotTest { + @Test + void weakeningRepairsUndercountFromSurvivingCountedOwnerSlots() { + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeHash referent = new RuntimeHash(); + referent.blessId = NameNormalizer.getBlessId("FinalWeakOwner::Undercounted"); + RuntimeScalar observer = referent.createAnonymousReference(); + WeakRefRegistry.weaken(observer); + + RuntimeScalar weakenedOwner = new RuntimeScalar(); + RuntimeScalar survivingOwner = new RuntimeScalar(); + MyVarCleanupStack.register(weakenedOwner); + MyVarCleanupStack.register(survivingOwner); + weakenedOwner.set(referent.createAnonymousReference()); + survivingOwner.set(referent.createAnonymousReference()); + try { + assertEquals(2, referent.countedOwnerSlotCount()); + referent.refCount = 1; // Model a transient selective-count undercount. + + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + WeakRefRegistry.weaken(weakenedOwner); + + assertEquals(1, referent.refCount); + assertEquals(1, referent.countedOwnerSlotCount()); + assertTrue(WeakRefRegistry.isweak(observer)); + assertEquals(0, measurement.stats().rootQueries, + "the surviving counted slot repairs the count without a root walk"); + assertEquals(0, measurement.stats().snapshotsBuilt); + assertEquals(0, measurement.stats().scalarReachabilityQueries); + } + } finally { + if (WeakRefRegistry.isweak(weakenedOwner)) { + WeakRefRegistry.unweaken(weakenedOwner); + } + if (WeakRefRegistry.isweak(observer)) WeakRefRegistry.unweaken(observer); + MyVarCleanupStack.unregister(weakenedOwner); + MyVarCleanupStack.unregister(survivingOwner); + weakenedOwner.set(new RuntimeScalar()); + survivingOwner.set(new RuntimeScalar()); + observer.set(new RuntimeScalar()); + } + } + } + +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/WeakeningPrunesStaleUntrackedOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/WeakeningPrunesStaleUntrackedOwnerSlotTest.java new file mode 100644 index 0000000000..c21d2bbb69 --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/WeakeningPrunesStaleUntrackedOwnerSlotTest.java @@ -0,0 +1,63 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Tag("unit") +class WeakeningPrunesStaleUntrackedOwnerSlotTest { + @Test + void staleUntrackedCellDoesNotPreserveWeakReferentAfterFinalCountedOwnerLeaves() { + PerlRuntime runtime = new PerlRuntime(); + RuntimeArray lexicalOwner = new RuntimeArray(); + RuntimeArray target = new RuntimeArray(); + RuntimeScalar staleCell = referenceTo(target); + RuntimeScalar weakObserver = referenceTo(target); + + try (PerlRuntime.Binding ignored = runtime.bind()) { + boolean previousMortalListState = MortalList.isActive(); + MortalList.setActive(true); + lexicalOwner.localBindingExists = true; + lexicalOwner.elements.add(staleCell); + PerlOwnerSlot staleOwner = staleCell.untrackedReferenceOwnerSlot(); + assertNotNull(staleOwner); + target.refCount = 1; + weakObserver.refCountOwned = true; + + try { + lexicalOwner.localBindingExists = false; + try (MortalList.ReachabilityQueryMeasurement measurement = + MortalList.measureReachabilityQueries()) { + WeakRefRegistry.weaken(weakObserver); + + assertFalse(staleOwner.isActive(), + "weakening must retire an untracked slot with no live durable cell"); + assertFalse(WeakRefRegistry.isweak(weakObserver), + "the stale slot must not strand the final weak observer"); + assertEquals(RuntimeScalarType.UNDEF, weakObserver.type); + assertEquals(Integer.MIN_VALUE, target.refCount); + assertEquals(0, measurement.stats().rootQueries); + assertEquals(0, measurement.stats().snapshotsBuilt); + } + } finally { + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + lexicalOwner.localBindingExists = false; + MortalList.scopeExitCleanupArray(lexicalOwner); + MortalList.setActive(previousMortalListState); + MortalList.invalidateAllRootSnapshots(); + } + } + } + + private static RuntimeScalar referenceTo(RuntimeBase referent) { + RuntimeScalar scalar = new RuntimeScalar(); + scalar.type = RuntimeScalarType.REFERENCE; + scalar.value = referent; + return scalar; + } +} diff --git a/src/test/java/org/perlonjava/runtime/runtimetypes/WeaklyTrackedUndefOwnerSlotTest.java b/src/test/java/org/perlonjava/runtime/runtimetypes/WeaklyTrackedUndefOwnerSlotTest.java new file mode 100644 index 0000000000..e7a2d86f4e --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/runtimetypes/WeaklyTrackedUndefOwnerSlotTest.java @@ -0,0 +1,73 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +class WeaklyTrackedUndefOwnerSlotTest { + @Test + void undefiningOneUntrackedCellPreservesTheOtherOwnerUntilItsRelease() { + Assumptions.assumeFalse(ReachabilityWalker.correctiveSweepsEnabled(), + "the owner-slot transition must work without corrective sweeps"); + + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + RuntimeArray target = new RuntimeArray(); + RuntimeScalar firstOwner = referenceTo(target); + RuntimeScalar secondOwner = referenceTo(target); + RuntimeScalar weakObserver = referenceTo(target); + int mark = MyVarCleanupStack.pushMark(); + try { + MyVarCleanupStack.register(firstOwner); + MyVarCleanupStack.register(secondOwner); + + PerlOwnerSlot firstSlot = firstOwner.untrackedReferenceOwnerSlot(); + PerlOwnerSlot secondSlot = secondOwner.untrackedReferenceOwnerSlot(); + assertEquals(-1, target.refCount); + assertTrue(firstSlot.isActive()); + assertTrue(secondSlot.isActive()); + + WeakRefRegistry.weaken(weakObserver); + assertEquals(WeakRefRegistry.WEAKLY_TRACKED, target.refCount); + + firstOwner.undefine(); + + assertFalse(firstSlot.isActive(), "undef releases this cell's owner exactly once"); + assertTrue(secondSlot.isActive(), "the other scalar cell still owns the referent"); + assertTrue(WeakRefRegistry.isweak(weakObserver)); + assertNotEquals(RuntimeBase.PerlLifecycleState.DESTROYED, + target.perlLifecycleState()); + + secondOwner.undefine(); + + assertFalse(secondSlot.isActive()); + assertFalse(WeakRefRegistry.isweak(weakObserver), + "the final owner release clears the weak observer"); + assertEquals(RuntimeBase.PerlLifecycleState.DESTROYED, + target.perlLifecycleState()); + } finally { + if (WeakRefRegistry.isweak(weakObserver)) { + WeakRefRegistry.unweaken(weakObserver); + } + RuntimeScalar.scopeExitCleanup(firstOwner); + RuntimeScalar.scopeExitCleanup(secondOwner); + MyVarCleanupStack.unregister(firstOwner); + MyVarCleanupStack.unregister(secondOwner); + MyVarCleanupStack.popMark(mark); + } + } + } + + private static RuntimeScalar referenceTo(RuntimeBase referent) { + RuntimeScalar scalar = new RuntimeScalar(); + scalar.type = RuntimeScalarType.REFERENCE; + scalar.value = referent; + return scalar; + } +} diff --git a/src/test/resources/unit/caller_db_args_alias_slot.t b/src/test/resources/unit/caller_db_args_alias_slot.t new file mode 100644 index 0000000000..7b69126413 --- /dev/null +++ b/src/test/resources/unit/caller_db_args_alias_slot.t @@ -0,0 +1,17 @@ +use strict; +use warnings; +use Test::More tests => 2; + +sub write_through_caller_args { + package DB; + () = caller(0); + $DB::args[0] = 'changed through caller alias'; +} + +my $value = 'before'; +write_through_caller_args($value); + +is($value, 'changed through caller alias', + 'caller exposes the original scalar slot'); +is($DB::args[0], 'changed through caller alias', + 'caller retains the aliased scalar value'); diff --git a/src/test/resources/unit/closure_capture_explicit_return.t b/src/test/resources/unit/closure_capture_explicit_return.t new file mode 100644 index 0000000000..273b872957 --- /dev/null +++ b/src/test/resources/unit/closure_capture_explicit_return.t @@ -0,0 +1,40 @@ +use strict; +use warnings; +use Test::More tests => 6; +use Scalar::Util qw(weaken); + +my $destroyed = 0; +my $weak_node; + +{ + package Local::ExplicitReturnCapture; + sub DESTROY { ++$destroyed } +} + +sub discard_captured_lexical_with_explicit_return { + my $node = bless {}, 'Local::ExplicitReturnCapture'; + $weak_node = $node; + weaken($weak_node); + my $callback = sub { $node }; + return; +} + +discard_captured_lexical_with_explicit_return(); +is($destroyed, 1, 'captured lexical is released after explicit return'); +ok(!defined $weak_node, 'weak reference clears after explicit return'); + +$destroyed = 0; +$weak_node = undef; +sub return_callback_owning_aggregate { + my @nodes = (bless {}, 'Local::ExplicitReturnCapture'); + $weak_node = $nodes[0]; + weaken($weak_node); + return sub { scalar @nodes }; +} + +my $callback = return_callback_owning_aggregate(); +is($callback->(), 1, 'returned closure retains its captured aggregate'); +is($destroyed, 0, 'captured aggregate survives the returning call frame'); +undef $callback; +is($destroyed, 1, 'captured aggregate is released with the returned closure'); +ok(!defined $weak_node, 'weak reference clears after returned closure release'); diff --git a/src/test/resources/unit/closure_capture_scope_exit.t b/src/test/resources/unit/closure_capture_scope_exit.t new file mode 100644 index 0000000000..f0adfda8e0 --- /dev/null +++ b/src/test/resources/unit/closure_capture_scope_exit.t @@ -0,0 +1,23 @@ +use strict; +use warnings; +use Test::More tests => 2; +use Scalar::Util qw(weaken); + +my $destroyed = 0; +my $weak_node; +{ + package Local::CapturedScopeValue; + sub DESTROY { ++$destroyed } +} + +sub discard_captured_lexical { + my $node = bless {}, 'Local::CapturedScopeValue'; + $weak_node = $node; + weaken($weak_node); + my $callback = sub { $node }; + $callback->(); +} + +discard_captured_lexical(); +is($destroyed, 1, 'captured lexical value is destroyed after its closure leaves scope'); +ok(!defined $weak_node, 'weak reference clears when the captured lexical is released'); diff --git a/src/test/resources/unit/issue_1860_growing_lexical_output.t b/src/test/resources/unit/issue_1860_growing_lexical_output.t new file mode 100644 index 0000000000..047582cb9f --- /dev/null +++ b/src/test/resources/unit/issue_1860_growing_lexical_output.t @@ -0,0 +1,35 @@ +use strict; +use warnings; +use Test::More tests => 4; + +my $width = 4; +my $depth = 5; +my @output; + +sub tumble { + my ($level, $path, $values, $payload) = @_; + if ($level == $depth) { + push @output, [ $path, $values, $payload ]; + return; + } + + for my $variant (1 .. $width) { + tumble( + $level + 1, + [ @$path, $variant ], + [ @$values, $variant ], + { %$payload }, + ); + } +} + +tumble(0, [], [], { payload => 'kept' }); + +is(scalar @output, $width ** $depth, + 'recursive callbacks append every result to the growing lexical output'); +is_deeply($output[0][0], [ (1) x $depth ], + 'the first retained path survives recursive scope cleanup'); +is_deeply($output[-1][0], [ (4) x $depth ], + 'the last retained path survives recursive scope cleanup'); +is($output[-1][2]{payload}, 'kept', + 'the final callback retains its payload'); diff --git a/src/test/resources/unit/mojolicious_controller_destroy_after_finish.t b/src/test/resources/unit/mojolicious_controller_destroy_after_finish.t new file mode 100644 index 0000000000..771ff9ce3b --- /dev/null +++ b/src/test/resources/unit/mojolicious_controller_destroy_after_finish.t @@ -0,0 +1,54 @@ +use strict; +use warnings; +use Test::More; + +BEGIN { + $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll'; + eval { + require IO::Socket::INET; + require Mojolicious::Lite; + Mojolicious::Lite->import; + require Test::Mojo; + require Mojo::IOLoop; + 1; + } or plan skip_all => 'Mojolicious and Test::Mojo are required'; + + my $probe = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => 0, + Listen => 1, + Proto => 'tcp', + ) or plan skip_all => 'loopback sockets are unavailable'; + close $probe; +} + +package MojoliciousControllerDestroyAfterFinish; +use Mojo::Base 'Mojolicious::Controller'; +sub DESTROY { shift->stash->{destroyed} = 1 } + +package main; +app->controller_class('MojoliciousControllerDestroyAfterFinish'); +get '/longpoll' => sub { + my $controller = shift; + $controller->on(finish => sub { shift->stash->{finished} = 1 }); + $controller->write_chunk('P' => sub { + shift->write_chunk('oll!' => sub { shift->write_chunk('') }); + }); +}; + +my $stash; +my $tick_count = 0; +my $timer = Mojo::IOLoop->recurring(0.1 => sub { ++$tick_count }); +my $test = Test::Mojo->new; +$test->app->plugins->once(before_dispatch => sub { $stash = shift->stash }); +$test->get_ok('/longpoll')->status_is(200)->content_is('Poll!'); + +Mojo::IOLoop->one_tick while !$stash->{finished} && $tick_count < 50; +is($stash->{finished}, 1, 'long-poll response finish event runs'); + +$tick_count = 0; +Mojo::IOLoop->one_tick while !$stash->{destroyed} && $tick_count < 50; +Mojo::IOLoop->remove($timer); +ok($stash->{destroyed}, 'controller is destroyed after response finish'); + +done_testing; diff --git a/src/test/resources/unit/mojolicious_websocket_controller_release.t b/src/test/resources/unit/mojolicious_websocket_controller_release.t new file mode 100644 index 0000000000..9d5c2a8183 --- /dev/null +++ b/src/test/resources/unit/mojolicious_websocket_controller_release.t @@ -0,0 +1,51 @@ +use strict; +use warnings; + +BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } + +use Test::More; + +BEGIN { + $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll'; + eval { + require IO::Socket::INET; + require Mojolicious::Lite; + Mojolicious::Lite->import; + require Test::Mojo; + require Mojo::IOLoop; + 1; + } or plan skip_all => 'Mojolicious and Test::Mojo are required'; + + my $probe = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => 0, + Listen => 1, + Proto => 'tcp', + ) or plan skip_all => 'loopback sockets are unavailable'; + close $probe; +} + +package WebSocketLifetimeController; +use Mojo::Base 'Mojolicious::Controller'; +sub DESTROY { $main::controller_destroyed = 1 } + +package main; +our $timer_id; +our $controller_destroyed = 0; +app->controller_class('WebSocketLifetimeController'); +websocket '/lifetime' => sub { + my $controller = shift; + $timer_id = Mojo::IOLoop->recurring(0.05 => sub { $controller->send('tick') }); +}; + +my $test = Test::Mojo->new; +$test->websocket_ok('/lifetime')->message_ok->message_is('tick'); +ok(!$controller_destroyed, 'controller stays alive while its WebSocket is open'); +Mojo::IOLoop->remove($timer_id); +$test->finish_ok; + +my $wait_expired = 0; +Mojo::IOLoop->timer(0.25 => sub { $wait_expired = 1 }); +Mojo::IOLoop->one_tick until $controller_destroyed || $wait_expired; +ok($controller_destroyed, 'controller is released after its WebSocket closes'); +done_testing; diff --git a/src/test/resources/unit/package_nested_untracked_aggregate_weak_lifetime.t b/src/test/resources/unit/package_nested_untracked_aggregate_weak_lifetime.t new file mode 100644 index 0000000000..0625e9382d --- /dev/null +++ b/src/test/resources/unit/package_nested_untracked_aggregate_weak_lifetime.t @@ -0,0 +1,23 @@ +use strict; +use warnings; +use Scalar::Util qw(isweak weaken); +use Test::More tests => 4; + +our @package_root; +my $weak; + +{ + my $target = []; + @package_root = ($target, $target); + $weak = $target; + weaken($weak); +} + +ok(isweak($weak), 'the observer is weak after first weaken'); +ok(defined($weak), 'package array elements keep the target alive after lexical exit'); + +$package_root[0] = undef; +ok(defined($weak), 'one remaining package element keeps the target alive'); + +$package_root[1] = undef; +ok(!defined($weak), 'the final package element release clears the weak observer'); diff --git a/src/test/resources/unit/refcount/active_callback_owner_release.t b/src/test/resources/unit/refcount/active_callback_owner_release.t new file mode 100644 index 0000000000..a3368fdead --- /dev/null +++ b/src/test/resources/unit/refcount/active_callback_owner_release.t @@ -0,0 +1,44 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(weaken); +our ($active, $weak_callback, $weak_guard, $destroyed); +$destroyed = 0; +{ package ActiveCallGuard; sub DESTROY { ++$main::destroyed } } +sub make_callback { + my ($throw) = @_; + my $guard = bless { value => 1 }, 'ActiveCallGuard'; + $weak_guard = $guard; + weaken($weak_guard); + return sub { + my ($depth) = @_; + if ($depth) { + $active->(0); + ok(defined $weak_callback, 'outer active call still owns the callback after inner return'); + ok(defined $weak_guard, 'outer call still owns the captured guard'); + } else { + undef $active; + ok(defined $weak_callback, 'active invocation owns callback after the last scalar is cleared'); + ok(defined $weak_guard, 'active callback keeps its captured guard'); + } + die "callback failed\n" if $throw; + return $guard->{value}; + }; +} +$active = make_callback(0); +$weak_callback = $active; +weaken($weak_callback); +$active->(1); +ok(!defined $weak_callback, 'final returning invocation releases its callback'); +ok(!defined $weak_guard, 'final callback release drops its captured guard'); +is($destroyed, 1, 'recursive dispatch destroys the captured guard exactly once'); + +$active = make_callback(1); +$weak_callback = $active; +weaken($weak_callback); +eval { $active->(0) }; +like($@, qr/callback failed/, 'callback exception propagates'); +ok(!defined $weak_callback, 'exception unwind releases its active callback owner'); +ok(!defined $weak_guard, 'exception unwind releases captured data'); +is($destroyed, 2, 'each captured guard is destroyed once'); +done_testing(); diff --git a/src/test/resources/unit/refcount/aggregate_owner_release_preserves_lexical_contents.t b/src/test/resources/unit/refcount/aggregate_owner_release_preserves_lexical_contents.t new file mode 100644 index 0000000000..7bf627cec1 --- /dev/null +++ b/src/test/resources/unit/refcount/aggregate_owner_release_preserves_lexical_contents.t @@ -0,0 +1,26 @@ +use strict; +use warnings; +use Test::More tests => 2; +use Scalar::Util qw(weaken); + +my @weak_nodes; +my $root = bless {}, 'Local::AggregateOwnerNode'; +push @weak_nodes, $root; +weaken($weak_nodes[-1]); + +my @children; +for (1 .. 3) { + my $child = bless {}, 'Local::AggregateOwnerNode'; + push @weak_nodes, $child; + weaken($weak_nodes[-1]); + push @children, $child; +} + +$root->{children} = \@children; +is(scalar(grep { defined $_ } @weak_nodes), 4, + 'the lexical array and parent hash keep all child values alive'); + +$root = undef; +@children = (); +is(scalar(grep { defined $_ } @weak_nodes), 0, + 'dropping both strong owners releases all children'); diff --git a/src/test/resources/unit/refcount/argument_array_clear_borrowed_cells.t b/src/test/resources/unit/refcount/argument_array_clear_borrowed_cells.t new file mode 100644 index 0000000000..f22f9a9302 --- /dev/null +++ b/src/test/resources/unit/refcount/argument_array_clear_borrowed_cells.t @@ -0,0 +1,24 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(weaken isweak); +our @destroyed; +{ package BorrowedArgumentGuard; sub DESTROY { push @main::destroyed, $_[0]{name} } } +sub clear_and_weaken_copy { + my ($copy, $how) = @_; + if ($how eq 'assign') { @_ = () } else { undef @_ } + weaken($copy); + ok(isweak($copy), "$how leaves the independently weakened copy alive"); +} +for my $how ('assign', 'undef') { + my $owner = bless { name => $how }, 'BorrowedArgumentGuard'; + my $weak = $owner; + weaken($weak); + clear_and_weaken_copy($owner, $how); + ok(defined $weak, "$how of arguments preserves the caller's counted owner"); + is($owner->{name}, $how, "$how preserves the caller's value"); + undef $owner; + ok(!defined $weak, "$how leaves exactly one caller owner to release"); +} +is_deeply(\@destroyed, ['assign', 'undef'], 'caller owners are destroyed exactly once'); +done_testing(); diff --git a/src/test/resources/unit/refcount/argument_evaluation_return.t b/src/test/resources/unit/refcount/argument_evaluation_return.t new file mode 100644 index 0000000000..5ed4043039 --- /dev/null +++ b/src/test/resources/unit/refcount/argument_evaluation_return.t @@ -0,0 +1,60 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(weaken); + +our (%weak, %destroyed, $entered); +$entered = 0; +{ package ArgumentReturnGuard; sub DESTROY { ++$main::destroyed{$_[0]{name}} } } +{ package ArgumentReturnReceiver; sub accept { ++$main::entered } } +sub make_argument { + my ($name) = @_; + my $object = bless { name => $name }, 'ArgumentReturnGuard'; + $weak{$name} = $object; + weaken($weak{$name}); + return $object; +} +sub escape_scalar { + ArgumentReturnReceiver->accept(make_argument('scalar'), do { return 'escaped' }); + die 'unreachable'; +} +is(escape_scalar(), 'escaped', 'return during argument evaluation leaves the containing sub'); +is($entered, 0, 'return during arguments never enters the callee'); +ok(!defined $weak{scalar}, 'return releases the abandoned argument'); +is($destroyed{scalar}, 1, 'return destroys the abandoned argument once'); + +sub escape_reference { + ArgumentReturnReceiver->accept(make_argument('reference'), do { + return make_argument('result'); + }); + die 'unreachable'; +} +my $result = escape_reference(); +is($entered, 0, 'reference return also bypasses the callee'); +ok(!defined $weak{reference}, 'reference return releases the abandoned argument'); +is($destroyed{reference}, 1, 'reference return destroys the abandoned argument once'); +ok(defined $weak{result}, 'receiving caller owns the returned reference'); +is($result->{name}, 'result', 'returned reference keeps its contents'); +undef $result; +ok(!defined $weak{result}, 'returned reference is released by its receiver'); +is($destroyed{result}, 1, 'returned reference is destroyed once'); + +sub escape_map { + my @unused = map { + ArgumentReturnReceiver->accept(make_argument('map'), do { + return make_argument('map-result'); + }); + } 1; + die 'unreachable'; +} +my $map_result = escape_map(); +is($entered, 0, 'non-local map return bypasses the callee'); +ok(!defined $weak{map}, 'non-local return releases the abandoned argument'); +is($destroyed{map}, 1, 'non-local return destroys the abandoned argument once'); +ok(defined $weak{'map-result'}, 'non-local return preserves its receiving owner'); +is($map_result->{name}, 'map-result', 'non-local return preserves result contents'); +undef $map_result; +ok(!defined $weak{'map-result'}, 'receiver releases the non-local return result'); +is($destroyed{'map-result'}, 1, 'non-local return result is destroyed once'); + +done_testing(); diff --git a/src/test/resources/unit/refcount/argument_evaluation_unwind.t b/src/test/resources/unit/refcount/argument_evaluation_unwind.t new file mode 100644 index 0000000000..46371f0b3a --- /dev/null +++ b/src/test/resources/unit/refcount/argument_evaluation_unwind.t @@ -0,0 +1,60 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(weaken); + +our (%weak, %destroyed, $entered); +$entered = 0; +{ + package ArgumentUnwindGuard; + sub DESTROY { ++$main::destroyed{$_[0]{name}} } +} +{ + package ArgumentUnwindReceiver; + sub accept { ++$main::entered; return scalar @_ } + sub reject { ++$main::entered; die "callee failed\n" } +} +sub make_argument { + my ($name) = @_; + my $object = bless { name => $name }, 'ArgumentUnwindGuard'; + $weak{$name} = $object; + weaken($weak{$name}); + return $object; +} +sub make_pair { + my $object = make_argument('pair'); + return ($object, $object); +} + +eval { ArgumentUnwindReceiver->accept(make_argument('abandoned'), die "later failed\n") }; +like($@, qr/later failed/, 'later argument failure propagates'); +is($entered, 0, 'failure during arguments never enters the callee'); +ok(!defined $weak{abandoned}, 'abandoned earlier argument releases its referent'); +is($destroyed{abandoned}, 1, 'abandoned referent is destroyed exactly once'); + +eval { ArgumentUnwindReceiver->accept(make_pair(), die "pair failed\n") }; +like($@, qr/pair failed/, 'failure after a repeated return list propagates'); +is($entered, 0, 'failed list arguments never enter the callee'); +ok(!defined $weak{pair}, 'all repeated argument owners are released'); +is($destroyed{pair}, 1, 'repeated argument edges destroy their referent once'); + +my $result = ArgumentUnwindReceiver->accept(make_argument('outer'), do { + eval { ArgumentUnwindReceiver->accept(make_argument('inner'), die "inner failed\n") }; + like($@, qr/inner failed/, 'inner argument failure is caught locally'); + ok(!defined $weak{inner}, 'inner abandoned argument is released'); + ok(defined $weak{outer}, 'inner unwinding preserves the outer staged argument'); + 1; +}); +is($result, 3, 'outer call receives its invocant and both arguments'); +is($entered, 1, 'only the outer call entered the callee'); +is($destroyed{inner}, 1, 'inner referent is destroyed once'); +ok(!defined $weak{outer}, 'successful outer call releases its temporary argument'); +is($destroyed{outer}, 1, 'outer referent is destroyed once'); + +eval { ArgumentUnwindReceiver->reject(make_argument('callee')) }; +like($@, qr/callee failed/, 'callee exception propagates'); +is($entered, 2, 'throwing callee was entered once'); +ok(!defined $weak{callee}, 'callee exception releases the argument'); +is($destroyed{callee}, 1, 'callee exception destroys the argument once'); + +done_testing(); diff --git a/src/test/resources/unit/refcount/blessed_code_state_resurrection.t b/src/test/resources/unit/refcount/blessed_code_state_resurrection.t new file mode 100644 index 0000000000..3f5ff6f1e9 --- /dev/null +++ b/src/test/resources/unit/refcount/blessed_code_state_resurrection.t @@ -0,0 +1,37 @@ +use strict; +use warnings; +use feature 'state'; +use Scalar::Util qw(weaken); +use Test::More; +our ($rescued, $weak, $initialized, $code_destroyed, $metadata_destroyed); +our @during_destroy; +($initialized, $code_destroyed, $metadata_destroyed) = (0, 0, 0); +{ + package CodeStateMetadata; + sub DESTROY { ++$main::metadata_destroyed } + package ResurrectedStateCode; + sub DESTROY { + push @main::during_destroy, defined($main::weak) ? 'live' : 'gone'; + ++$main::code_destroyed; + $main::rescued = $_[0] if $main::code_destroyed == 1; + } +} +my $callback = do { + my $seed = 41; + sub { state $metadata = bless { serial => ++$initialized, value => $seed }, 'CodeStateMetadata'; $metadata } +}; +bless $callback, 'ResurrectedStateCode'; +$weak = $callback->(); weaken($weak); +is($initialized, 1, 'state initialized once'); +undef $callback; +is($code_destroyed, 1, 'first CODE owner release invokes DESTROY'); +ok(defined $weak, 'resurrected CODE retains state'); +is($rescued->()->{serial}, 1, 'resurrected CODE uses original state'); +is($initialized, 1, 'resurrection does not initialize replacement state'); +is($metadata_destroyed, 0, 'state metadata survives first CODE DESTROY'); +undef $rescued; +is($code_destroyed, 2, 'last resurrected CODE owner invokes DESTROY again'); +is_deeply(\@during_destroy, ['live', 'live'], 'state survives both CODE destructor callbacks'); +ok(!defined $weak, 'final CODE destruction releases weak state'); +is($metadata_destroyed, 1, 'state metadata destroyed exactly once'); +done_testing(); diff --git a/src/test/resources/unit/refcount/closure_state_owner_release.t b/src/test/resources/unit/refcount/closure_state_owner_release.t new file mode 100644 index 0000000000..54e36751d1 --- /dev/null +++ b/src/test/resources/unit/refcount/closure_state_owner_release.t @@ -0,0 +1,54 @@ +use strict; +use warnings; +use feature 'state'; +use Scalar::Util qw(weaken); +use Test::More; + +sub scalar_callback { + my ($seed) = @_; + return sub { state $metadata = { value => $seed }; return $metadata }; +} +my $callback = scalar_callback(1); +my $weak = $callback->(); weaken($weak); +my $keeper = $callback; +undef $callback; +ok(defined $weak, 'another CODE owner preserves scalar state'); +is($keeper->()->{value}, 1, 'remaining CODE owner can use scalar state'); +undef $keeper; +ok(!defined $weak, 'final CODE owner releases scalar state'); + +sub array_callback { + my ($seed) = @_; + return sub { state @items = ({ value => $seed }); return \@items }; +} +$callback = array_callback(2); +$weak = $callback->()->[0]; weaken($weak); +undef $callback; +ok(!defined $weak, 'final CODE owner releases state array contents'); + +sub hash_callback { + my ($seed) = @_; + return sub { state %items = (item => { value => $seed }); return \%items }; +} +$callback = hash_callback(3); +my $escaped = $callback->(); +$weak = $escaped->{item}; weaken($weak); +undef $callback; +ok(defined $weak, 'escaped state hash retains its contents after CODE release'); +is($escaped->{item}{value}, 3, 'escaped state hash remains usable'); +undef $escaped; +ok(!defined $weak, 'last escaped hash owner releases its contents'); + +sub scalar_cell_callback { + my ($seed) = @_; + return sub { state $item = { value => $seed }; return \$item }; +} +$callback = scalar_cell_callback(4); +$escaped = $callback->(); +$weak = $$escaped; weaken($weak); +undef $callback; +ok(defined $weak, 'escaped state scalar cell retains its contents'); +is($$escaped->{value}, 4, 'escaped state scalar cell remains usable'); +undef $escaped; +ok(!defined $weak, 'last scalar cell owner releases its contents'); +done_testing(); diff --git a/src/test/resources/unit/refcount/code_return_argument_snapshot.t b/src/test/resources/unit/refcount/code_return_argument_snapshot.t new file mode 100644 index 0000000000..30f253c121 --- /dev/null +++ b/src/test/resources/unit/refcount/code_return_argument_snapshot.t @@ -0,0 +1,21 @@ +use strict; +use warnings; +use Test::More tests => 4; + +my $callback = sub { 'before' }; +sub fetch_callback { return $callback } +sub replace_callback { $callback = sub { 'after' }; return 1 } +sub invoke_first { return $_[0]->() } + +is(invoke_first(fetch_callback(), replace_callback()), 'before', + 'a returned coderef is copied before the next argument changes its source'); +is($callback->(), 'after', 'the source coderef remains independently writable'); + +$callback = sub { 'list-before' }; +sub fetch_pair { return ($callback, $callback) } +sub replace_pair { $callback = sub { 'list-after' }; return 1 } +sub invoke_pair { return join '/', $_[0]->(), $_[1]->() } + +is(invoke_pair(fetch_pair(), replace_pair()), 'list-before/list-before', + 'each returned list element keeps its value through later argument evaluation'); +is($callback->(), 'list-after', 'list return copies do not alias the source cell'); diff --git a/src/test/resources/unit/refcount/code_template_owner_lifetime.t b/src/test/resources/unit/refcount/code_template_owner_lifetime.t new file mode 100644 index 0000000000..ee88032da5 --- /dev/null +++ b/src/test/resources/unit/refcount/code_template_owner_lifetime.t @@ -0,0 +1,27 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(weaken); +my $plain = sub { 42 }; +my $weak = $plain; +weaken($weak); +$plain->(); +undef $plain; +ok(defined $weak, 'noncapturing anonymous CODE remains owned by its optree'); +my $constant = sub () { 42 }; +$weak = $constant; +weaken($weak); +$constant->(); +undef $constant; +ok(defined $weak, 'constant anonymous CODE remains owned by its optree'); +sub make_counter { + my $value = 0; + return sub { ++$value }; +} +my $capturing = make_counter(); +$weak = $capturing; +weaken($weak); +is($capturing->(), 1, 'capturing callback remains callable'); +undef $capturing; +ok(!defined $weak, 'last counted owner releases an inactive capturing callback'); +done_testing(); diff --git a/src/test/resources/unit/refcount/compiled_glob_state_owner_release.t b/src/test/resources/unit/refcount/compiled_glob_state_owner_release.t new file mode 100644 index 0000000000..d5d08d6836 --- /dev/null +++ b/src/test/resources/unit/refcount/compiled_glob_state_owner_release.t @@ -0,0 +1,14 @@ +use strict; +use warnings; +use feature 'state'; +use Scalar::Util qw(weaken); +use Test::More; +our $initialized = 0; +sub pinned_metadata { state $metadata = { serial => ++$initialized }; $metadata } +my $weak = pinned_metadata(); weaken($weak); +delete $main::{pinned_metadata}; +ok(defined $weak, 'compiled GV owner preserves state after stash name deletion'); +is(pinned_metadata()->{serial}, 1, 'compiled call uses original state after stash deletion'); +is($initialized, 1, 'stash deletion does not initialize replacement state'); +ok(defined $weak, 'original state remains owned by the compiled call site'); +done_testing(); diff --git a/src/test/resources/unit/refcount/conditional_collection_owner_release.t b/src/test/resources/unit/refcount/conditional_collection_owner_release.t new file mode 100644 index 0000000000..52fe143a99 --- /dev/null +++ b/src/test/resources/unit/refcount/conditional_collection_owner_release.t @@ -0,0 +1,25 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(weaken); +our $destroyed = 0; +{ package ConditionalCollectionOwner; sub DESTROY { ++$main::destroyed } } +sub array_result { + if (my @items = (bless({value => 1}, 'ConditionalCollectionOwner'))) { @items } +} +sub hash_result { + if (my %items = (one => bless({value => 2}, 'ConditionalCollectionOwner'))) { %items } +} +my @array = array_result(); +is($array[0]{value}, 1, 'conditional array returns its elements'); +is($destroyed, 0, 'returned array child remains alive'); +my $weak = $array[0]; weaken($weak); +@array = (); +ok(!defined $weak, 'conditional array child releases after caller array'); +my %hash = hash_result(); +is($hash{one}{value}, 2, 'conditional hash returns its pairs'); +is($destroyed, 1, 'returned hash child remains alive'); +$weak = $hash{one}; weaken($weak); +%hash = (); +ok(!defined $weak, 'conditional hash child releases after caller hash'); +done_testing(); diff --git a/src/test/resources/unit/refcount/conditional_loop_local_owner_release.t b/src/test/resources/unit/refcount/conditional_loop_local_owner_release.t new file mode 100644 index 0000000000..36d89d5f65 --- /dev/null +++ b/src/test/resources/unit/refcount/conditional_loop_local_owner_release.t @@ -0,0 +1,65 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(weaken); +our $destroyed = 0; +{ package ConditionalLoopOwner; sub DESTROY { ++$main::destroyed } } +sub consume_items { + my ($owner) = @_; + if (my $items = delete $owner->{items}) { + $_->{error} = 7 for values %$items; + } +} +my $guard = bless {}, 'ConditionalLoopOwner'; +my $owner = { items => { one => $guard } }; +my $weak_items = $owner->{items}; +weaken($weak_items); +my $weak_guard = $guard; +weaken($weak_guard); +undef $guard; +consume_items($owner); +ok(!exists $owner->{items}, 'conditional initializer removes the source container slot'); +ok(!defined $weak_items, 'conditional lexical releases its container after postfix loop'); +ok(!defined $weak_guard, 'conditional container releases its child slot'); +is($destroyed, 1, 'conditional scope destroys the child exactly once'); +is_deeply($owner, {}, 'callee cleanup preserves the caller aggregate'); +sub condition_hash_result { + if (my %items = (one => bless({ value => 9 }, 'ConditionalLoopOwner'))) { + \%items; + } +} +my $hash_destroyed_before = $destroyed; +my $returned = condition_hash_result(); +is($destroyed, $hash_destroyed_before, 'conditional return keeps its child alive before caller acquisition'); +is($returned->{one}{value}, 9, 'conditional result preserves a reference to its lexical hash'); +my $weak_returned_child = $returned->{one}; +weaken($weak_returned_child); +undef $returned; +ok(!defined $weak_returned_child, 'caller releases the returned conditional hash and its child'); +sub conditional_capture { + if (my $item = bless { value => 12 }, 'ConditionalLoopOwner') { + sub { $item->{value} }; + } +} +my $callback = conditional_capture(); +is($callback->(), 12, 'implicit conditional return keeps its captured lexical alive'); +my $before = $destroyed; +undef $callback; +is($destroyed, $before + 1, 'final callback owner releases its conditional capture'); +{ + package FalseConditionalOwner; + use overload bool => sub { 0 }, fallback => 1; + sub DESTROY { ++$main::destroyed } +} +sub false_condition_result { + if (my $item = bless { value => 17 }, 'FalseConditionalOwner') { + die "false conditional branch executed\n"; + } +} +my $false_result = false_condition_result(); +is($false_result->{value}, 17, 'untaken branch preserves its false reference-valued condition'); +my $weak_false = $false_result; +weaken($weak_false); +undef $false_result; +ok(!defined $weak_false, 'caller releases the false conditional result'); +done_testing(); diff --git a/src/test/resources/unit/refcount/conditional_tied_result_owner_release.t b/src/test/resources/unit/refcount/conditional_tied_result_owner_release.t new file mode 100644 index 0000000000..4ba5bc7192 --- /dev/null +++ b/src/test/resources/unit/refcount/conditional_tied_result_owner_release.t @@ -0,0 +1,51 @@ +use strict; +use warnings; +use Test::More; +our ($destroyed, @events); +$destroyed = 0; +{ + package ConditionalTiedResult; + sub TIESCALAR { bless { value => 41 }, shift } + sub FETCH { push @main::events, 'FETCH'; $_[0]{value} } + sub STORE { push @main::events, 'STORE'; $_[0]{value} = $_[1] } + sub DESTROY { push @main::events, 'DESTROY'; ++$main::destroyed } +} +sub scalar_result { + if (tie my $value, 'ConditionalTiedResult') { $value } +} +is(scalar_result(), 41, 'conditional tied scalar returns its fetched value'); +is_deeply(\@events, ['FETCH', 'DESTROY'], 'rvalue fetch precedes conditional tie destruction'); +@events = (); +sub lvalue_result :lvalue { + if (tie my $value, 'ConditionalTiedResult') { $value } +} +lvalue_result() = 73; +is_deeply(\@events, ['STORE', 'DESTROY'], 'lvalue store precedes conditional tie destruction'); +is($destroyed, 2, 'both conditional tied handlers release exactly once'); +{ + package ConditionalHashLvalueOwner; + sub TIEHASH { bless {}, shift } + sub FETCH { push @main::events, 'FETCH'; 0 } + sub STORE { push @main::events, 'STORE' } + sub DESTROY { push @main::events, 'DESTROY'; ++$main::destroyed } + package ConditionalArrayLvalueOwner; + sub TIEARRAY { bless {}, shift } + sub FETCHSIZE { 1 } + sub FETCH { push @main::events, 'FETCH'; 0 } + sub STORE { push @main::events, 'STORE' } + sub DESTROY { push @main::events, 'DESTROY'; ++$main::destroyed } +} +sub hash_lvalue_result :lvalue { + if (tie my %value, 'ConditionalHashLvalueOwner') { $value{slot} } +} +sub array_lvalue_result :lvalue { + if (tie my @value, 'ConditionalArrayLvalueOwner') { $value[0] } +} +@events = (); +hash_lvalue_result() = 83; +is_deeply(\@events, ['STORE', 'DESTROY'], 'returned hash proxy retains its magic through STORE'); +@events = (); +array_lvalue_result() = 97; +is_deeply(\@events, ['STORE', 'DESTROY'], 'returned array proxy retains its magic through STORE'); +is($destroyed, 4, 'all conditional tied owners retire once'); +done_testing(); diff --git a/src/test/resources/unit/refcount/destroy_exception_resurrection.t b/src/test/resources/unit/refcount/destroy_exception_resurrection.t new file mode 100644 index 0000000000..a6189fa139 --- /dev/null +++ b/src/test/resources/unit/refcount/destroy_exception_resurrection.t @@ -0,0 +1,55 @@ +use strict; +use warnings; +use Scalar::Util qw(weaken); +use Test::More tests => 11; + +our ($saved, $calls, $warnings); +our @reblessed; +{ + package Local::DyingResurrector; + sub DESTROY { + ++$main::calls; + $main::saved = $_[0] if $main::calls == 1; + die "dying after rescue\n"; + } +} +{ + package Local::DyingRebless; + sub DESTROY { + push @main::reblessed, 'original'; + bless $_[0], 'Local::AfterDyingRebless'; + die "dying after rebless\n"; + } +} +{ + package Local::AfterDyingRebless; + sub DESTROY { push @main::reblessed, 'replacement' } +} + +local $SIG{__WARN__} = sub { $warnings .= $_[0] }; +my $value = bless {}, 'Local::DyingResurrector'; +my $weak = $value; +weaken($weak); +$@ = 'caller state'; +$value = undef; +ok(defined($saved), 'a dying destructor can publish a strong owner'); +ok(defined($weak), 'weak observers survive real resurrection'); +is($calls, 1, 'the initial destructor runs once'); +$saved = undef; +ok(!defined($weak), 'the final resurrected owner clears weak observers'); +is($calls, 2, 'releasing resurrection invokes DESTROY again'); +is($warnings, "\t(in cleanup) dying after rescue\n" x 2, + 'both dying destructor calls report cleanup warnings'); +is($@, 'caller state', 'destruction preserves the caller exception state'); + +my $reblessed = bless {}, 'Local::DyingRebless'; +my $weak_reblessed = $reblessed; +weaken($weak_reblessed); +$reblessed = undef; +is_deeply(\@reblessed, [qw(original replacement)], + 'a dying destructor still dispatches destruction for its new class'); +ok(!defined($weak_reblessed), 'reblessed destruction clears weak observers'); +is($warnings, ("\t(in cleanup) dying after rescue\n" x 2) + . "\t(in cleanup) dying after rebless\n", + 'reblessing reports the original cleanup exception once'); +is($@, 'caller state', 'reblessed destruction preserves the caller exception state'); diff --git a/src/test/resources/unit/refcount/eval_callback_return_owner_release.t b/src/test/resources/unit/refcount/eval_callback_return_owner_release.t new file mode 100644 index 0000000000..4989ed2899 --- /dev/null +++ b/src/test/resources/unit/refcount/eval_callback_return_owner_release.t @@ -0,0 +1,58 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(weaken); +our ($weak, $destroyed); +$destroyed = 0; +{ package EvalCallbackOwner; sub DESTROY { ++$main::destroyed } } +sub eval_number { + my $result = eval { + my $guard = bless { value => 41 }, 'EvalCallbackOwner'; + $weak = $guard; + weaken($weak); + map { return 42 } 1; + return 99; + }; + return [$result, 'after eval']; +} +is_deeply(eval_number(), [42, 'after eval'], 'callback return exits eval and resumes its surrounding sub'); +ok(!defined $weak, 'callback return releases eval lexicals before the caller resumes'); +is($destroyed, 1, 'eval lexical destructor runs exactly once'); +sub eval_reference { + my $result = eval { + my $guard = bless { value => 73 }, 'EvalCallbackOwner'; + $weak = $guard; + weaken($weak); + map { return $guard } 1; + return undef; + }; + return $result; +} +my $result = eval_reference(); +is($result->{value}, 73, 'callback return preserves an eval-local reference'); +ok(defined $weak, 'receiving caller owns the returned reference'); +is($destroyed, 1, 'returned eval lexical stays alive'); +undef $result; +ok(!defined $weak, 'last caller owner releases the returned eval lexical'); +is($destroyed, 2, 'each eval object is destroyed once'); +{ + package EvalOwnerReceiver; + sub new { bless {}, shift } + sub never { die "callback should have returned before entering method\n" } +} +sub eval_argument { + my $result = eval { + my $guard = bless { value => 95 }, 'EvalCallbackOwner'; + $weak = $guard; + weaken($weak); + EvalOwnerReceiver->new->never(map { return $guard } 1); + }; + return $result; +} +$result = eval_argument(); +is($result->{value}, 95, 'eval return survives abandoned method argument evaluation'); +ok(defined $weak, 'abandoned method cleanup preserves the eval return owner'); +undef $result; +ok(!defined $weak, 'abandoned method return releases at its last caller'); +is($destroyed, 3, 'abandoned method return destroys the object once'); +done_testing(); diff --git a/src/test/resources/unit/refcount/issue_1649_package_nested_untracked_weak_owner.t b/src/test/resources/unit/refcount/issue_1649_package_nested_untracked_weak_owner.t new file mode 100644 index 0000000000..e30707eb8d --- /dev/null +++ b/src/test/resources/unit/refcount/issue_1649_package_nested_untracked_weak_owner.t @@ -0,0 +1,55 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(weaken isweak); + +{ + package PackageNestedUntrackedWeakOwner; + our @root; +} + +my $weak; +{ + my $target = {}; + @PackageNestedUntrackedWeakOwner::root = ($target); + $weak = $target; + weaken($weak); + + ok(isweak($weak), 'the observer is weak while the package aggregate owns the target'); + ok(defined($weak), 'a nested package owner keeps the weak referent alive'); +} + +ok(defined($weak), 'leaving the original lexical preserves the package-owned referent'); +@PackageNestedUntrackedWeakOwner::root = (); +ok(!defined($weak), 'removing the final package owner clears the weak observer'); + +my $strong_after_unweaken; +my $weak_probe; +my $has_unweaken = eval { + require Scalar::Util; + Scalar::Util->import('unweaken'); + 1; +}; + +SKIP: { + skip 'Scalar::Util::unweaken is unavailable', 3 unless $has_unweaken; + + { + my $target = {}; + @PackageNestedUntrackedWeakOwner::root = ($target); + $strong_after_unweaken = $target; + weaken($strong_after_unweaken); + $weak_probe = $target; + weaken($weak_probe); + Scalar::Util::unweaken($strong_after_unweaken); + } + + @PackageNestedUntrackedWeakOwner::root = (); + ok(defined($strong_after_unweaken), + 'unweakened scalar remains a strong owner after package removal'); + ok(defined($weak_probe), 'the weak probe survives while the unweakened owner lives'); + undef($strong_after_unweaken); + ok(!defined($weak_probe), 'dropping the unweakened owner clears the weak probe'); +} + +done_testing(); diff --git a/src/test/resources/unit/refcount/issue_1649_temporary_closure_argument_owner.t b/src/test/resources/unit/refcount/issue_1649_temporary_closure_argument_owner.t new file mode 100644 index 0000000000..ab03cdf36e --- /dev/null +++ b/src/test/resources/unit/refcount/issue_1649_temporary_closure_argument_owner.t @@ -0,0 +1,32 @@ +use strict; +use warnings; +use Test::More tests => 5; +use Scalar::Util qw(weaken); + +our $saved_callback; +our $destroyed = 0; + +{ + package Issue1649::ArgumentOwner; + sub DESTROY { $main::destroyed++ } +} + +sub retain_and_call { + my ($callback) = @_; + my $captured = $callback->(); + ok(defined $captured, 'temporary callback can read its capture in the callee'); + $saved_callback = $callback; +} + +my $weak; +{ + my $object = bless {}, 'Issue1649::ArgumentOwner'; + weaken($weak = $object); + retain_and_call(sub { $object }); + ok(defined $weak, 'the saved callback retains its capture after caller scope exit'); + is($destroyed, 0, 'the object is not destroyed while the callback is saved'); +} + +undef $saved_callback; +ok(!defined $weak, 'releasing the saved callback releases its captured object'); +is($destroyed, 1, 'the captured object is destroyed exactly once'); diff --git a/src/test/resources/unit/refcount/issue_1794_controller_owner_release.t b/src/test/resources/unit/refcount/issue_1794_controller_owner_release.t new file mode 100644 index 0000000000..1eab25998f --- /dev/null +++ b/src/test/resources/unit/refcount/issue_1794_controller_owner_release.t @@ -0,0 +1,66 @@ +use strict; +use warnings; + +use Test::More; +use Scalar::Util qw(weaken); + +{ + package Issue1794Transaction; + + sub new { bless { events => {} }, shift } + + sub on { + my ($self, $name, $callback) = @_; + push @{$self->{events}{$name}}, $callback; + return $self; + } + + sub finish { shift->emit('finish') } + + sub emit { + my ($self, $name, @args) = @_; + $_->($self, @args) for @{$self->{events}{$name} // []}; + return $self; + } +} + +{ + package Issue1794Controller; + use Scalar::Util qw(weaken); + + sub new { + my ($class, $tx) = @_; + my $self = bless { tx => $tx }, $class; + weaken($self->{tx}); + return $self; + } + + sub on { + my ($self, $name, $callback) = @_; + my $tx = $self->{tx}; + return $tx->on($name => sub { shift; $self->$callback(@_) }); + } + + sub DESTROY { $main::destroyed++ } +} + +our $destroyed = 0; +my $weak_controller; +{ + my $tx = Issue1794Transaction->new; + my $controller = Issue1794Controller->new($tx); + $weak_controller = $controller; + weaken($weak_controller); + + $controller->on(finish => sub { shift }); + $tx->finish; + undef $controller; + + is($destroyed, 0, 'transaction callback keeps controller alive before teardown'); + undef $tx; +} + +is($destroyed, 1, 'controller DESTROY runs when its transaction releases callbacks'); +is($weak_controller, undef, 'controller weak reference clears after transaction teardown'); + +done_testing; diff --git a/src/test/resources/unit/refcount/lexical_return_slot_owner_release.t b/src/test/resources/unit/refcount/lexical_return_slot_owner_release.t new file mode 100644 index 0000000000..24be455cdc --- /dev/null +++ b/src/test/resources/unit/refcount/lexical_return_slot_owner_release.t @@ -0,0 +1,33 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(weaken); +our $destroyed=0; +{ package LexicalReturnOwner; sub DESTROY { ++$main::destroyed } } +sub make { my $value=bless {}, 'LexicalReturnOwner'; $value } +my $object=make();my $weak=$object;weaken($weak);undef $object; +ok(!defined $weak,'return from the lexical register releases its owner'); +is($destroyed,1,'returned lexical referent destroyed exactly once'); +sub make_named { + my ($name) = @_; + my $value = bless { name => $name }, 'LexicalReturnOwner'; + return $value; +} +$object = make_named('later-local'); +$weak = $object; +weaken($weak); +undef $object; +ok(!defined $weak, 'earlier local cleanup preserves the later lexical owner until its release'); +is($destroyed, 2, 'a return shared with a later lexical slot is destroyed once'); +sub make_named_implicit { + my ($name) = @_; + my $value = bless { name => $name }, 'LexicalReturnOwner'; + $value; +} +$object = make_named_implicit('implicit-later-local'); +$weak = $object; +weaken($weak); +undef $object; +ok(!defined $weak, 'implicit return also uses a separate result register'); +is($destroyed, 3, 'multi-local implicit return releases its owner once'); +done_testing(); diff --git a/src/test/resources/unit/refcount/local_array_return_owner_release.t b/src/test/resources/unit/refcount/local_array_return_owner_release.t new file mode 100644 index 0000000000..5dc460db55 --- /dev/null +++ b/src/test/resources/unit/refcount/local_array_return_owner_release.t @@ -0,0 +1,49 @@ +use strict; +use warnings; +use Scalar::Util qw(weaken); +use Test::More tests => 8; + +our $destroyed = 0; +{ + package Local::ArrayReturnOwner; + sub DESTROY { ++$main::destroyed } +} + +sub return_local_array { + my @items; + push @items, $_[0]; + return @items; +} + +{ + my $value = bless {}, 'Local::ArrayReturnOwner'; + my $weak = $value; + weaken($weak); + return_local_array($value); + $value = undef; + ok(!defined($weak), 'a discarded local array return releases its element owner'); + is($destroyed, 1, 'void return does not defer destruction past the last real owner'); +} + +{ + my $value = bless {}, 'Local::ArrayReturnOwner'; + my $weak = $value; + weaken($weak); + my $count = return_local_array($value); + $value = undef; + is($count, 1, 'a scalar array return reports its element count'); + ok(!defined($weak), 'a scalar array return retains no element owner'); + is($destroyed, 2, 'scalar return releases one additional object'); +} + +{ + my $value = bless {}, 'Local::ArrayReturnOwner'; + my $weak = $value; + weaken($weak); + my @returned = return_local_array($value); + $value = undef; + ok(defined($weak), 'a list return transfers ownership to the receiving array'); + @returned = (); + ok(!defined($weak), 'clearing the received array releases its element owner'); + is($destroyed, 3, 'list return releases one additional object exactly once'); +} diff --git a/src/test/resources/unit/refcount/localized_code_state_owner_release.t b/src/test/resources/unit/refcount/localized_code_state_owner_release.t new file mode 100644 index 0000000000..905a3faba4 --- /dev/null +++ b/src/test/resources/unit/refcount/localized_code_state_owner_release.t @@ -0,0 +1,33 @@ +use strict; +use warnings; +use feature 'state'; +use Scalar::Util qw(weaken); +use Test::More; +sub localized_metadata { state $metadata = { value => 10 }; $metadata } +my $original = localized_metadata(); weaken($original); +my ($temporary, $nested); +{ + local *localized_metadata = do { + my $seed = 20; + sub { state $metadata = { value => $seed }; $metadata } + }; + $temporary = localized_metadata(); weaken($temporary); + ok(defined $original, 'saved CODE slot preserves original state'); + is(localized_metadata()->{value}, 20, 'localized CODE owns independent state'); + { + local *localized_metadata = do { + my $seed = 30; + sub { state $metadata = { value => $seed }; $metadata } + }; + $nested = localized_metadata(); weaken($nested); + ok(defined $temporary, 'nested localization preserves outer temporary state'); + is(localized_metadata()->{value}, 30, 'nested CODE owns independent state'); + } + ok(!defined $nested, 'restoring outer slot releases nested CODE state'); + is(localized_metadata()->{value}, 20, 'outer localized CODE is restored'); +} +ok(!defined $temporary, 'restoring original slot releases temporary CODE state'); +is(localized_metadata()->{value}, 10, 'original CODE state is restored'); +undef *localized_metadata; +ok(!defined $original, 'removing final original slot releases original state'); +done_testing(); diff --git a/src/test/resources/unit/refcount/named_code_state_owner_release.t b/src/test/resources/unit/refcount/named_code_state_owner_release.t new file mode 100644 index 0000000000..dd6bf345e8 --- /dev/null +++ b/src/test/resources/unit/refcount/named_code_state_owner_release.t @@ -0,0 +1,35 @@ +use strict; +use warnings; +use feature 'state'; +use Scalar::Util qw(weaken); +use Test::More; +sub named_metadata { state $metadata = { value => 1 }; $metadata } +my $weak = named_metadata(); weaken($weak); +my $keeper = \&named_metadata; +undef *named_metadata; +ok(defined $weak, 'external CODE reference retains its state after glob removal'); +is($keeper->()->{value}, 1, 'retained CODE state remains usable without its glob'); +undef $keeper; +ok(!defined $weak, 'final CODE reference releases its state'); +sub aliased_metadata { state $metadata = { value => 2 }; $metadata } +{ + no warnings 'once'; + *alias_metadata = \&aliased_metadata; +} +$weak = aliased_metadata(); weaken($weak); +undef *aliased_metadata; +ok(defined $weak, 'alias CODE slot retains the shared state'); +is(alias_metadata()->{value}, 2, 'alias remains callable after original glob removal'); +undef *alias_metadata; +ok(!defined $weak, 'last CODE alias releases its state'); +our $active_weak; +sub active_metadata { + state $metadata = { value => 3 }; + $active_weak = $metadata; weaken($active_weak); + undef *active_metadata; + ok(defined $active_weak, 'active call keeps state after its glob is removed'); + return $metadata->{value}; +} +is(active_metadata(), 3, 'active call can finish after glob removal'); +ok(!defined $active_weak, 'last active invocation releases its state'); +done_testing(); diff --git a/src/test/resources/unit/refcount/native_parser_callback_owner_release.t b/src/test/resources/unit/refcount/native_parser_callback_owner_release.t new file mode 100644 index 0000000000..4182518b4f --- /dev/null +++ b/src/test/resources/unit/refcount/native_parser_callback_owner_release.t @@ -0,0 +1,53 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(weaken); +use HTML::Parser; +our $calls = 0; +sub observe_event { ++$calls } +sub fail_event { die "event callback failed\n" } +{ + package NativeParserMethodOwner; + our @ISA = qw(HTML::Parser); + sub observe { ++$main::calls } +} +my $parser = HTML::Parser->new(api_version => 3); +$parser->handler(text => \&observe_event, 'self,text'); +my $weak = $parser; +weaken($weak); +$parser->parse('text'); +$parser->eof; +ok($calls > 0, 'native parser invokes the CODE callback'); +undef $parser; +ok(!defined $weak, 'CODE callback argument arrays release their parser owner'); +$calls = 0; +$parser = NativeParserMethodOwner->new(api_version => 3); +$parser->handler(text => 'observe', 'self,text'); +$weak = $parser; +weaken($weak); +$parser->parse('text'); +$parser->eof; +ok($calls > 0, 'native parser invokes the method callback'); +undef $parser; +ok(!defined $weak, 'method callback argument arrays release their parser owner'); +$parser = HTML::Parser->new(api_version => 3); +$parser->handler(start => \&fail_event, 'self,attr'); +$weak = $parser; +weaken($weak); +eval { $parser->parse('

') }; +like($@, qr/event callback failed/, 'native callback exception propagates'); +undef $parser; +ok(!defined $weak, 'exception unwind releases callback argument owners'); +my @events; +$parser = HTML::Parser->new(api_version => 3); +$parser->handler(text => \@events, 'self,text'); +$weak = $parser; +weaken($weak); +$parser->parse('text'); +$parser->eof; +ok(@events > 0, 'accumulator callback stores its event data'); +undef $parser; +ok(defined $weak, 'accumulated self arguments remain genuine strong owners'); +@events = (); +ok(!defined $weak, 'clearing accumulated events breaks the parser cycle'); +done_testing(); diff --git a/src/test/resources/unit/refcount/oneshot_callback_self_owner_release.t b/src/test/resources/unit/refcount/oneshot_callback_self_owner_release.t new file mode 100644 index 0000000000..ca71ce31e9 --- /dev/null +++ b/src/test/resources/unit/refcount/oneshot_callback_self_owner_release.t @@ -0,0 +1,79 @@ +use strict; +use warnings; +use feature 'current_sub'; +use Test::More tests => 5; +use Scalar::Util qw(weaken); + +our $destroyed = 0; + +{ + package OneShotOwner; + + sub new { bless {}, shift } + sub touch { 1 } + sub DESTROY { ++$main::destroyed } + + package OneShotEmitter; + use Scalar::Util qw(weaken); + + sub new { bless { events => {} }, shift } + + sub on { + push @{$_[0]{events}{$_[1]}}, $_[2]; + return $_[2]; + } + + sub once { + my ($self, $name, $cb) = @_; + weaken $self; + my $wrapper = sub { + $self->unsubscribe($name => __SUB__); + $cb->(@_); + }; + $self->on($name => $wrapper); + return $wrapper; + } + + sub unsubscribe { + my ($self, $name, $cb) = @_; + if ($cb) { + $self->{events}{$name} = [grep { $cb ne $_ } @{$self->{events}{$name}}]; + delete $self->{events}{$name} unless @{$self->{events}{$name}}; + } + else { + delete $self->{events}{$name}; + } + return $self; + } + + sub subscribers { shift->{events}{shift()} //= [] } + + sub emit { + my ($self, $name) = (shift, shift); + if (my $subscribers = $self->{events}{$name}) { + for my $cb (@$subscribers) { $self->$cb(@_) } + } + return $self; + } +} + +my $emitter = OneShotEmitter->new; +my ($weak_owner, $weak_wrapper); +{ + my $owner = OneShotOwner->new; + $weak_owner = $owner; + weaken($weak_owner); + + my $wrapper = $emitter->once(tick => sub { shift; $owner->touch }); + $weak_wrapper = $wrapper; + weaken($weak_wrapper); +} + +ok defined $weak_owner, 'one-shot callback keeps its captured owner before emission'; +ok defined $weak_wrapper, 'one-shot callback wrapper is registered before emission'; + +$emitter->emit('tick'); + +is $destroyed, 1, 'removing an active self-referential callback releases its capture'; +ok !defined $weak_owner, 'captured owner weak reference clears after one-shot emission'; +ok !defined $weak_wrapper, 'one-shot wrapper weak reference clears after emission'; diff --git a/src/test/resources/unit/refcount/package_nested_untracked_aggregate_release.t b/src/test/resources/unit/refcount/package_nested_untracked_aggregate_release.t new file mode 100644 index 0000000000..5c547d0bc8 --- /dev/null +++ b/src/test/resources/unit/refcount/package_nested_untracked_aggregate_release.t @@ -0,0 +1,31 @@ +use strict; +use warnings; +use Test::More tests => 6; +use Scalar::Util qw(weaken isweak); + +our @PACKAGE_NESTED_OWNER_ROOT; +our %PACKAGE_NESTED_OWNER_ROOT_HASH; + +my $target = []; +my $weak = $target; +$PACKAGE_NESTED_OWNER_ROOT[0] = $target; +weaken($weak); +undef $target; + +ok(isweak($weak), 'the observer is weak'); +ok(defined($weak), 'the package array keeps the nested aggregate alive'); + +@PACKAGE_NESTED_OWNER_ROOT = (); +ok(!defined($weak), 'clearing the package array releases the final strong slot'); + +my $hash_target = []; +my $hash_weak = $hash_target; +$PACKAGE_NESTED_OWNER_ROOT_HASH{child} = $hash_target; +weaken($hash_weak); +undef $hash_target; + +ok(isweak($hash_weak), 'the hash observer is weak'); +ok(defined($hash_weak), 'the package hash keeps the nested aggregate alive'); + +delete $PACKAGE_NESTED_OWNER_ROOT_HASH{child}; +ok(!defined($hash_weak), 'deleting the package hash entry releases the final strong slot'); diff --git a/src/test/resources/unit/refcount/pad_constant_cv_owner_lifetime.t b/src/test/resources/unit/refcount/pad_constant_cv_owner_lifetime.t new file mode 100644 index 0000000000..52f98255ac --- /dev/null +++ b/src/test/resources/unit/refcount/pad_constant_cv_owner_lifetime.t @@ -0,0 +1,82 @@ +use strict; +use warnings; +use Scalar::Util qw(weaken refaddr); +use Test::More; + +{ + no warnings 'redefine'; + sub pad_constant_owner_target { return \q(owned) } +} + +my $old_cv = \&pad_constant_owner_target; +my $strong = $old_cv->(); +my $weak = $strong; +weaken($weak); +undef $strong; + +{ + no warnings 'redefine'; + *pad_constant_owner_target = sub { return undef }; +} + +ok(defined $weak, 'the old CV keeps its pad constant alive after glob replacement'); +my $same_constant = $old_cv->(); +ok(defined $weak && refaddr($weak) == refaddr($same_constant), + 'the retained CV still returns the same constant scalar'); +undef $same_constant; +ok(defined $weak, 'a returned weakly observed constant survives call cleanup'); + +undef $old_cv; +ok(!defined $weak, 'releasing the last old-CV owner reaps its pad constant'); + +{ + no warnings 'redefine'; + sub pad_constant_external_owner_target { return \q(external) } +} + +my $external_cv = \&pad_constant_external_owner_target; +my $external_strong = $external_cv->(); +my $external_weak = $external_strong; +weaken($external_weak); +{ + no warnings 'redefine'; + undef &pad_constant_external_owner_target; +} +undef $external_cv; +ok(defined $external_weak, + 'a caller reference keeps the constant alive after its CV is released'); +undef $external_strong; +ok(!defined $external_weak, + 'the constant is reaped when its final caller reference leaves'); + +{ + no warnings 'redefine'; + sub pad_constant_factory { return sub { return \q(shared) } } +} + +my $factory = \&pad_constant_factory; +my $first_closure = $factory->(); +my $template_weak = $first_closure->(); +weaken($template_weak); +undef $first_closure; +ok(defined $template_weak, + 'the factory CV keeps its nested optree constant alive between closures'); + +my $second_closure = $factory->(); +my $second_constant = $second_closure->(); +ok(defined $template_weak && refaddr($template_weak) == refaddr($second_constant), + 'later closure instances share the nested optree constant identity'); +undef $second_constant; +undef $second_closure; +ok(defined $template_weak, + 'the factory template remains the owner after a closure instance is released'); + +{ + no warnings 'redefine'; + undef &pad_constant_factory; +} +undef $factory; +ok(!defined $template_weak, + 'releasing the factory and its final closure owner reaps the nested constant'); + +done_testing(); diff --git a/src/test/resources/unit/refcount/repeated_container_edges_release.t b/src/test/resources/unit/refcount/repeated_container_edges_release.t new file mode 100644 index 0000000000..8edcda9425 --- /dev/null +++ b/src/test/resources/unit/refcount/repeated_container_edges_release.t @@ -0,0 +1,32 @@ +use strict; +use warnings; +use Scalar::Util qw(weaken); +use Test::More tests => 6; + +our $destroyed = 0; +{ + package Local::RepeatedContainerEdge; + sub DESTROY { ++$main::destroyed } +} + +my $value = bless {}, 'Local::RepeatedContainerEdge'; +my $weak = $value; +weaken($weak); +my $nested = [{ left => $value, right => $value }]; +$value = undef; +ok(defined($weak), 'two nested element slots keep their shared referent alive'); + +$nested = undef; +ok(!defined($weak), 'dropping the outer owner releases both nested element slots'); +is($destroyed, 1, 'the shared referent is destroyed exactly once'); + +my $cycle = bless {}, 'Local::RepeatedContainerEdge'; +$cycle->{self} = $cycle; +my $weak_cycle = $cycle; +weaken($weak_cycle); +$cycle = undef; +ok(defined($weak_cycle), 'a strong cycle survives the final external owner'); + +delete $weak_cycle->{self}; +ok(!defined($weak_cycle), 'breaking the cycle releases its final strong element'); +is($destroyed, 2, 'cycle cleanup runs one additional destructor'); diff --git a/src/test/resources/unit/refcount/scalar_cell_return_contents_owner.t b/src/test/resources/unit/refcount/scalar_cell_return_contents_owner.t new file mode 100644 index 0000000000..fda06cc724 --- /dev/null +++ b/src/test/resources/unit/refcount/scalar_cell_return_contents_owner.t @@ -0,0 +1,28 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(weaken); +our @destroyed; +{ package ReturnedCellChild; sub DESTROY { push @main::destroyed, $_[0]{name} } } +sub implicit_cell { + my ($name) = @_; + \(my $value = bless { name => $name }, 'ReturnedCellChild'); +} +sub explicit_cell { + my ($name) = @_; + my $value = bless { name => $name }, 'ReturnedCellChild'; + return \$value; +} +for my $factory (\&implicit_cell, \&explicit_cell) { + my $name = $factory == \&implicit_cell ? 'implicit' : 'explicit'; + my $cell = $factory->($name); + my $weak = $$cell; + weaken($weak); + is($$cell->{name}, $name, "$name return preserves the cell's contents"); + ok(defined $weak, "$name return owns its contents after lexical exit"); + $$cell = undef; + ok(!defined $weak, "$name return releases contents on assignment through the cell"); + is_deeply(\@destroyed, $name eq 'implicit' ? ['implicit'] : ['implicit', 'explicit'], + "$name cell contents are destroyed exactly once"); +} +done_testing(); diff --git a/src/test/resources/unit/refcount/scalar_cell_scope_owner_release.t b/src/test/resources/unit/refcount/scalar_cell_scope_owner_release.t new file mode 100644 index 0000000000..59ecd7690d --- /dev/null +++ b/src/test/resources/unit/refcount/scalar_cell_scope_owner_release.t @@ -0,0 +1,45 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(weaken); + +my $bootstrap = []; +weaken($bootstrap); +my $weak; +{ + my $value = 42; + $weak = \$value; + weaken($weak); + ok(defined $weak, 'lexical cell stays alive in scope'); + is($$weak, 42, 'lexical cell keeps its value'); + undef $value; + ok(defined $weak, 'undef changes contents while cell remains alive'); + ok(!defined $$weak, 'weak cell sees undef contents'); +} +ok(!defined $weak, 'final lexical owner releases its scalar cell'); + +my $strong; +{ + my $value = 'held'; + $strong = \$value; + $weak = \$value; + weaken($weak); +} +ok(defined $weak, 'escaped strong reference owns the cell after scope'); +is($$strong, 'held', 'escaped cell retains contents'); +undef $strong; +ok(!defined $weak, 'final escaped reference releases the cell'); + +my $callback; +{ + my $value = 7; + $weak = \$value; + weaken($weak); + $callback = sub { $value }; +} +ok(defined $weak, 'closure owns the captured scalar cell'); +is($callback->(), 7, 'captured cell retains contents'); +undef $callback; +ok(!defined $weak, 'final capture releases the scalar cell'); + +done_testing(); diff --git a/src/test/resources/unit/refcount/self_capture_cycle_owner.t b/src/test/resources/unit/refcount/self_capture_cycle_owner.t new file mode 100644 index 0000000000..7f949d1b05 --- /dev/null +++ b/src/test/resources/unit/refcount/self_capture_cycle_owner.t @@ -0,0 +1,39 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(weaken refaddr); +our ($weak_callback, $weak_guard, $destroyed); +$destroyed = 0; +{ package SelfCaptureGuard; sub DESTROY { ++$main::destroyed } } +sub install_cycle { + my $guard = bless { value => 1 }, 'SelfCaptureGuard'; + my $cycle; + $cycle = sub { + if ($_[0]) { + undef $cycle; + return $guard->{value}; + } + return $cycle; + }; + $weak_callback = $cycle; + weaken($weak_callback); + $weak_guard = $guard; + weaken($weak_guard); + return 1; +} +install_cycle(); +ok(defined $weak_callback, 'a real self-capture cycle retains its callback after lexical exit'); +SKIP: { + skip 'callback cycle was already lost', 7 unless defined $weak_callback; + ok(defined $weak_guard, 'the self-capture cycle owns its captured guard'); + my $strong = $weak_callback->(0); + is(refaddr($strong), refaddr($weak_callback), 'cycle callback returns its own CV'); + is($strong->(1), 1, 'cycle can be broken while its callback is running'); + ok(defined $weak_callback, 'external holder preserves callback after cycle break'); + ok(defined $weak_guard, 'external callback holder preserves captured data'); + undef $strong; + ok(!defined $weak_callback, 'final external owner releases the broken cycle'); + ok(!defined $weak_guard, 'broken cycle releases its captured guard'); +} +is($destroyed, 1, 'breaking the self-capture cycle destroys its guard exactly once'); +done_testing(); diff --git a/src/test/resources/unit/refcount/state_aggregate_alias_owner_release.t b/src/test/resources/unit/refcount/state_aggregate_alias_owner_release.t new file mode 100644 index 0000000000..8d8510518b --- /dev/null +++ b/src/test/resources/unit/refcount/state_aggregate_alias_owner_release.t @@ -0,0 +1,33 @@ +use strict; +use warnings; +use feature qw(state refaliasing declared_refs); +no warnings qw(experimental::refaliasing experimental::declared_refs); +use Scalar::Util qw(weaken); +use Test::More; +{ + my @source = ({ value => 21 }); + my $callback = do { + my $reference = \@source; + sub { \state @items = $reference; return $items[0] } + }; + my $weak = $callback->(); weaken($weak); + undef $callback; + ok(defined $weak, 'source array binding survives state alias release'); + is($source[0]{value}, 21, 'source array contents remain usable'); + @source = (); + ok(!defined $weak, 'source array clear releases final element owner'); +} +{ + my %source = (item => { value => 22 }); + my $callback = do { + my $reference = \%source; + sub { \state %items = $reference; return $items{item} } + }; + my $weak = $callback->(); weaken($weak); + undef $callback; + ok(defined $weak, 'source hash binding survives state alias release'); + is($source{item}{value}, 22, 'source hash contents remain usable'); + %source = (); + ok(!defined $weak, 'source hash clear releases final element owner'); +} +done_testing(); diff --git a/src/test/resources/unit/refcount/tied_handler_owner_release.t b/src/test/resources/unit/refcount/tied_handler_owner_release.t new file mode 100644 index 0000000000..a8098dea30 --- /dev/null +++ b/src/test/resources/unit/refcount/tied_handler_owner_release.t @@ -0,0 +1,83 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(weaken); +our $destroyed = 0; +{ + package CountedTieHandler; + sub TIEHASH { $_[1] } + sub TIEARRAY { $_[1] } + sub TIESCALAR { $_[1] } + sub DESTROY { ++$main::destroyed } +} +my (%hash, @array, $scalar); +my $handler = bless {}, 'CountedTieHandler'; +my $weak = $handler; +weaken($weak); +tie %hash, 'CountedTieHandler', $handler; +tie @array, 'CountedTieHandler', $handler; +tie $scalar, 'CountedTieHandler', $handler; +undef $handler; +ok(defined $weak, 'each tie keeps its handler after the source scalar is cleared'); +{ + no warnings 'untie'; + untie %hash; +} +ok(defined $weak, 'array and scalar ties retain the handler after hash untie'); +is($destroyed, 0, 'hash untie does not destroy a shared handler'); +{ + no warnings 'untie'; + untie @array; +} +ok(defined $weak, 'scalar tie retains the handler after array untie'); +is($destroyed, 0, 'array untie does not destroy a shared handler'); +untie $scalar; +ok(!defined $weak, 'last tie releases the handler'); +is($destroyed, 1, 'handler is destroyed exactly once'); +untie %hash; +untie @array; +untie $scalar; +is($destroyed, 1, 'repeated untie does not release another owner'); +{ + package CountedTieOuter; + sub TIEHASH { $_[1] } + sub DESTROY { + my $self = shift; + ++$main::destroyed unless tied(%$self); + } + sub wrap { + my $inner = shift; + my %outer; + tie %outer, __PACKAGE__, $inner; + bless \%outer, __PACKAGE__; + } +} +$destroyed = 0; +my $weak_inner; +{ + my $outer = (bless {}, 'CountedTieOuter')->wrap; + $weak_inner = tied(%$outer); + weaken($weak_inner); + ok(defined $weak_inner, 'tied outer owns its inner handler'); +} +ok(!defined $weak_inner, 'destroying outer releases its inner handler before the next statement'); +is($destroyed, 1, 'inner handler destructor runs exactly once after outer scope exit'); +{ + package BorrowedTieRestoration; + sub TIEHASH { bless {}, shift } + sub EXISTS { 0 } + sub FETCH { undef } + sub STORE { } + sub DELETE { die "restoring tied entry\n" } + sub DESTROY { ++$main::destroyed } +} +my $before = $destroyed; +eval { + my %tied; + tie %tied, 'BorrowedTieRestoration'; + local $tied{key} = 'value'; + die "body failure\n"; +}; +is($@, "restoring tied entry\n", 'local restoration can borrow the handler after its lexical owner retires'); +is($destroyed, $before + 1, 'exception unwind retires the handler owner once'); +done_testing(); diff --git a/src/test/resources/unit/weakly_tracked_undef_preserves_other_owner.t b/src/test/resources/unit/weakly_tracked_undef_preserves_other_owner.t new file mode 100644 index 0000000000..7dd9fe3b77 --- /dev/null +++ b/src/test/resources/unit/weakly_tracked_undef_preserves_other_owner.t @@ -0,0 +1,16 @@ +use strict; +use warnings; +use Test::More tests => 2; +use Scalar::Util qw(weaken); + +my $target = []; +my @owners = ($target, $target); +my $weak = $target; +weaken($weak); +undef $target; + +undef $owners[0]; +ok(defined $weak, 'one remaining strong cell keeps the weak target alive'); + +undef $owners[1]; +ok(!defined $weak, 'the final strong cell releases the weak target');