Skip to content

globals: make WeakRef and FinalizationRegistry actually weak #2656

Description

@andrewtdiz

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:

  1. Construct a WeakRef, drop all strong references to its target, force/drain GC in the test harness, and assert deref() can become undefined.
  2. Register a target with FinalizationRegistry, drop it, force/drain GC, and assert the cleanup callback receives the held value.
  3. Register with an unregister token, call unregister(token), then force/drain GC and assert no cleanup is delivered for that registration.
  4. 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"

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions