Summary
Perry exposes WeakRef and FinalizationRegistry, but the runtime implementation strongly retains targets and never fires finalization callbacks. That is shape-compatible for simple feature detection, but it diverges from Node semantics for memory-sensitive caches and cleanup code that relies on weak reachability.
Evidence
Node v25.9.0 behavior from a local shape probe:
typeof WeakRef === "function" with prototype methods constructor and deref.
typeof FinalizationRegistry === "function" with prototype methods constructor, register, and unregister.
new WeakRef({}).deref() returns the target while it is still strongly reachable.
new WeakRef(1) throws TypeError: WeakRef: invalid target.
new FinalizationRegistry(1) throws TypeError: FinalizationRegistry: cleanup must be callable.
FinalizationRegistry#unregister(token) returns true after a matching registration.
Current Perry evidence from origin/main:
crates/perry-runtime/src/weakref.rs documents the implementation as a pragmatic/stub implementation.
- The same file says
WeakRef holds a strong reference internally, so deref() always returns the wrapped value.
- The same file says
FinalizationRegistry stores registrations but never fires cleanup callbacks.
js_weakref_new(target) stores the target in a normal object field named __perry_wr_target, and js_weakref_deref() reads that field back.
js_finreg_new(callback) and js_finreg_register(...) store callback and registration arrays, but there is no GC integration that clears weak slots or enqueues cleanup jobs.
docs/src/language/limitations.md states the current caveat explicitly: WeakRef.deref() always returns the original target, FinalizationRegistry never fires cleanup callbacks, and weak collections keep keys alive.
- Existing fixtures such as
test_gap_weakref_finalization.ts cover construction/register/unregister shapes, not actual weak reachability or cleanup delivery.
I could not run a Perry binary in this checkout because there is no local target/release/perry, so the current-behavior claim is based on origin/main source and docs inspection.
Expected Node behavior
Perry should eventually integrate weak references with its GC so that:
WeakRef does not keep its target alive.
- After the target is collected,
weakRef.deref() can return undefined.
FinalizationRegistry can enqueue cleanup callbacks for collected registered targets, subject to Node/ECMAScript's nondeterministic timing guarantees.
unregister(token) prevents matching cleanup callbacks from running.
- Constructor and method validation continue to match Node's TypeError behavior.
Current Perry behavior
WeakRef is effectively a strong wrapper object. Registered targets are retained by ordinary object/array fields, so the GC cannot discover that they are weak, clear the references, or schedule finalization work. This can leak memory in weak-cache patterns and skips finalizer side effects that Node code may use for native resource bookkeeping.
Suggested test surface
Add tests that are tolerant of nondeterministic finalization but still validate the wiring when Perry exposes an explicit GC/test hook:
- Construct a
WeakRef, drop all strong references to its target, force/drain GC in the test harness, and assert deref() can become undefined.
- Register a target with
FinalizationRegistry, drop it, force/drain GC, and assert the cleanup callback receives the held value.
- Register with an unregister token, call
unregister(token), then force/drain GC and assert no cleanup is delivered for that registration.
- Preserve current shape tests for constructor validation,
register, and unregister return values.
Scope / non-goals
This is not about the existing feature-detection surface or the hidden-slot inspect fixes (#1766 / PR #2267). It is specifically about replacing the strong-reference stub with GC-integrated weak reachability and finalization semantics. WeakMap/WeakSet key retention is a closely related limitation, but this issue is scoped first to the global WeakRef and FinalizationRegistry APIs.
Duplicate search performed:
gh issue list --repo PerryTS/perry --state all --search "WeakRef FinalizationRegistry"
gh issue list --repo PerryTS/perry --state all --search "WeakRef deref collected FinalizationRegistry cleanup"
gh issue list --repo PerryTS/perry --state all --search "FinalizationRegistry register unregister cleanup callback"
gh issue list --repo PerryTS/perry --state all --search "weak reference WeakRef FinalizationRegistry"
gh pr list --repo PerryTS/perry --state all --search "WeakRef FinalizationRegistry"
gh pr list --repo PerryTS/perry --state all --search "WeakRef deref collected FinalizationRegistry cleanup"
Summary
Perry exposes
WeakRefandFinalizationRegistry, but the runtime implementation strongly retains targets and never fires finalization callbacks. That is shape-compatible for simple feature detection, but it diverges from Node semantics for memory-sensitive caches and cleanup code that relies on weak reachability.Evidence
Node v25.9.0 behavior from a local shape probe:
typeof WeakRef === "function"with prototype methodsconstructorandderef.typeof FinalizationRegistry === "function"with prototype methodsconstructor,register, andunregister.new WeakRef({}).deref()returns the target while it is still strongly reachable.new WeakRef(1)throwsTypeError: WeakRef: invalid target.new FinalizationRegistry(1)throwsTypeError: FinalizationRegistry: cleanup must be callable.FinalizationRegistry#unregister(token)returnstrueafter a matching registration.Current Perry evidence from
origin/main:crates/perry-runtime/src/weakref.rsdocuments the implementation as a pragmatic/stub implementation.WeakRefholds a strong reference internally, soderef()always returns the wrapped value.FinalizationRegistrystores registrations but never fires cleanup callbacks.js_weakref_new(target)stores the target in a normal object field named__perry_wr_target, andjs_weakref_deref()reads that field back.js_finreg_new(callback)andjs_finreg_register(...)store callback and registration arrays, but there is no GC integration that clears weak slots or enqueues cleanup jobs.docs/src/language/limitations.mdstates the current caveat explicitly:WeakRef.deref()always returns the original target,FinalizationRegistrynever fires cleanup callbacks, and weak collections keep keys alive.test_gap_weakref_finalization.tscover construction/register/unregister shapes, not actual weak reachability or cleanup delivery.I could not run a Perry binary in this checkout because there is no local
target/release/perry, so the current-behavior claim is based onorigin/mainsource and docs inspection.Expected Node behavior
Perry should eventually integrate weak references with its GC so that:
WeakRefdoes not keep its target alive.weakRef.deref()can returnundefined.FinalizationRegistrycan enqueue cleanup callbacks for collected registered targets, subject to Node/ECMAScript's nondeterministic timing guarantees.unregister(token)prevents matching cleanup callbacks from running.Current Perry behavior
WeakRefis effectively a strong wrapper object. Registered targets are retained by ordinary object/array fields, so the GC cannot discover that they are weak, clear the references, or schedule finalization work. This can leak memory in weak-cache patterns and skips finalizer side effects that Node code may use for native resource bookkeeping.Suggested test surface
Add tests that are tolerant of nondeterministic finalization but still validate the wiring when Perry exposes an explicit GC/test hook:
WeakRef, drop all strong references to its target, force/drain GC in the test harness, and assertderef()can becomeundefined.FinalizationRegistry, drop it, force/drain GC, and assert the cleanup callback receives the held value.unregister(token), then force/drain GC and assert no cleanup is delivered for that registration.register, andunregisterreturn values.Scope / non-goals
This is not about the existing feature-detection surface or the hidden-slot inspect fixes (#1766 / PR #2267). It is specifically about replacing the strong-reference stub with GC-integrated weak reachability and finalization semantics. WeakMap/WeakSet key retention is a closely related limitation, but this issue is scoped first to the global
WeakRefandFinalizationRegistryAPIs.Duplicate search performed:
gh issue list --repo PerryTS/perry --state all --search "WeakRef FinalizationRegistry"gh issue list --repo PerryTS/perry --state all --search "WeakRef deref collected FinalizationRegistry cleanup"gh issue list --repo PerryTS/perry --state all --search "FinalizationRegistry register unregister cleanup callback"gh issue list --repo PerryTS/perry --state all --search "weak reference WeakRef FinalizationRegistry"gh pr list --repo PerryTS/perry --state all --search "WeakRef FinalizationRegistry"gh pr list --repo PerryTS/perry --state all --search "WeakRef deref collected FinalizationRegistry cleanup"