feat(runtime): #2656 — make WeakMap/WeakSet actually weak#5468
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (5)
✅ Files skipped from review due to trivial changes (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughRewrites WeakMap/WeakSet runtime storage in ChangesWeakMap/WeakSet Weak-Entry Objects (
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/perry-runtime/src/weakref.rs`:
- Around line 751-758: The weak_entry_at function implicitly assumes that the
entries array only contains POINTER_TAG-tagged values (which have zeros in their
low 48 bits), but this assumption is not documented or validated. If future code
changes allow non-pointer tags like TAG_UNDEFINED or TAG_HOLE to be stored in
entries, the masked extraction would produce invalid non-null garbage pointers
that bypass null checks. Add explicit documentation to the weak_entry_at
function explaining why entries can only contain POINTER_TAG-tagged values, and
consider adding a defensive check that verifies the high bits of the extracted
value match POINTER_TAG before casting to ensure the extraction is safe against
future modifications.
In `@crates/perry/tests/issue_2656_weak_collections_actually_weak.rs`:
- Line 37: The run command using Command::new(&output) does not set the working
directory to the temporary directory like the compile command does, which can
cause the binary to fail if it expects to run from that specific directory. Add
`.current_dir(dir)` to the Command::new(&output) chain before calling .output(),
matching the pattern used in the compile command to ensure consistent working
directory context.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 65d1964a-37be-4d04-a7cb-701971c23247
📒 Files selected for processing (2)
crates/perry-runtime/src/weakref.rscrates/perry/tests/issue_2656_weak_collections_actually_weak.rs
| String::from_utf8_lossy(&compile.stdout), | ||
| String::from_utf8_lossy(&compile.stderr) | ||
| ); | ||
| let run = Command::new(&output).output().expect("run compiled binary"); |
There was a problem hiding this comment.
Set .current_dir(dir) when running the compiled binary.
The compile command correctly sets .current_dir(dir) (Line 24), but the run command does not. The upstream pattern in issue_4903_listen_callback_deferred.rs shows both compile and run commands should set the working directory to the temp directory. If the compiled binary expects to run from the temp directory (e.g., reads relative paths or creates temporary files), omitting this will cause incorrect behavior.
🔧 Proposed fix
- let run = Command::new(&output).output().expect("run compiled binary");
+ let run = Command::new(&output)
+ .current_dir(dir)
+ .output()
+ .expect("run compiled binary");📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let run = Command::new(&output).output().expect("run compiled binary"); | |
| let run = Command::new(&output) | |
| .current_dir(dir) | |
| .output() | |
| .expect("run compiled binary"); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/perry/tests/issue_2656_weak_collections_actually_weak.rs` at line 37,
The run command using Command::new(&output) does not set the working directory
to the temporary directory like the compile command does, which can cause the
binary to fail if it expects to run from that specific directory. Add
`.current_dir(dir)` to the Command::new(&output) chain before calling .output(),
matching the pattern used in the compile command to ensure consistent working
directory context.
WeakMap/WeakSet previously stored entries as plain `[key, value]` pair arrays that the GC traced strongly, so keys were never collected — weak in API only. Store each entry as a `CLASS_ID_WEAK_ENTRY` object whose field-0 key is a weak GC slot (skipped by the strong-edge scanners, exactly like a WeakRef target or a finalization record's target), with the value in field 1 (strong; `undefined` for a WeakSet). The post-mark weak pass tombstones entries whose key was collected — nulling both slots so the value is released — and lookups treat a tombstoned (undefined-key) entry as empty. `set` reuses tombstone slots and `delete` compacts them, bounding growth. This directly reuses the WeakRef/FinalizationRegistry weak-slot machinery (`is_weak_target_trace_slot`, `weak_target_should_clear`, the post-mark walk). A key/value reachable only through the collection is now collectible; live entries are retained and values released when their key dies. Verified under the default GC and the auto-evacuation policy. Out of scope: the `PERRY_GC_FORCE_EVACUATE` full-evacuation stress mode over-collects weak targets generally — FinalizationRegistry has the same limitation, and that mode is also subject to the separate strong-array bug #5467 (which causes the only crashes seen there). No production GC mode is affected. Regression test: crates/perry/tests/issue_2656_weak_collections_actually_weak.rs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…p/WeakSet weakness Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
685c831 to
2b8b278
Compare
…ring branch Second main sync to clear the version-bump re-conflict that left PR #5466 DIRTY (no CI runs on a conflicting PR). Conflicts: CHANGELOG (kept both, my entry -> v0.5.1198 above main's WeakMap 1197); strings_part2.rs auto-unioned the FFI decls; version 0.5.1197 -> 0.5.1198 (main already shipped 1197). Build green; no type-lowering code reconflicted. Claude-Session: https://claude.ai/code/session_019hwNPXCAWnjv5vddcznhFA
Fixes #2656 (the WeakMap/WeakSet portion; WeakRef + FinalizationRegistry were already weak).
Problem
WeakMap/WeakSet stored entries as plain
[key, value]pair arrays that the GC traced strongly, so keys reachable only through the collection were never collected — weak in name/API only. Repro: a key held only by a WeakMap stays alive acrossgc().Fix
Store each entry as a
CLASS_ID_WEAK_ENTRYobject:is_weak_target_trace_slot).undefinedfor a WeakSet (storing the member as the value too would pin it and defeat weakness).The existing post-mark weak pass tombstones entries whose key was collected (nulling both slots, so the value is released next cycle); lookups skip tombstoned (undefined-key) entries.
setreuses tombstone slots anddeletecompacts them, bounding growth. This is a direct reuse of the proven WeakRef/FinalizationRegistry machinery — no new GC phase.Behavior (verified)
Under default GC and the auto-evacuation policy (
PERRY_GEN_GC_EVACUATE=1):WeakRef+ churn +gc()).has/getkeep working;instanceof, iteration-placeholder inspect, and brand checks unaffected.Regression test
crates/perry/tests/issue_2656_weak_collections_actually_weak.rs(dead key collected, dead value released, dead WeakSet member collected, live entry + value retained, live member retained) is green.perry-runtimeweakref unit test + 15 weak/collectiontest_gap_*files pass; the existingissue_2656WeakRef/FinalizationRegistry GC test is unaffected.Scope note (FORCE_EVACUATE)
The
PERRY_GC_FORCE_EVACUATEfull-evacuation debug stress mode (gc-stress CI; tag-gated, skips on PRs) over-collects weak targets in general — FinalizationRegistry has the identical limitation (its records are likewise non-root entry objects with a weak field-0). Root cause:valid_ptrsis built pre-copy, and a weak wrapper that is itself evacuated isn't resolvable in the post-mark liveness pass; non-moved WeakRef wrappers sidestep it. Fixing it needs GC copy-machinery changes (repairing weak slots of evacuated objects) and is left as a follow-up. The crashes seen under that mode come from the separate, pre-existing strong-array-in-closure bug #5467, not this change. No production GC mode (default, auto-evacuation) is affected.🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
Bug Fixes
WeakMapandWeakSetso entries are truly weak: collected keys are cleared correctly, and empty/tombstoned slots are handled so live entries remain available while dead ones are released.Tests
WeakMap/WeakSetbehavior across garbage-collection churn, ensuring correctness for both keys and members.Documentation / Release
v0.5.1197.