From aa9ded3fde8fc4046d3b2767f862bc8a76ca0ac7 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 30 Sep 2026 20:34:04 +0100 Subject: [PATCH] WeakRef: the target is cleared once it has gone; weakref_basic asserts it WeakRef kept its target in a plain pointer field. The collector finds that pointer when it scans the WeakRef, so the target stayed reachable and the disappearing link was never cleared: after the last reference went and GC_gcollect() ran, deref() still gave the object. gc.h asks for a disguised pointer in the link location; the field now holds the target's address complemented (GC_HIDE_POINTER), which the collector sets to 0 when the target goes. weakref_basic only printed its results and checked for "ALL DONE", so it passed with the link never cleared. It now asserts: the object before a collection, undefined after it, and that a held object survives. The deref of a live object is in a function of its own, so no pointer to it is left in main's frame for the conservative stack scan. WeakRef and this test are gc only - under rc, none and own the compiler now rejects the GC_* calls (TypeScriptCompiler: "A call to the collector's API outside -mm=gc is a compile error"). Both say so in a comment. Co-Authored-By: Claude Opus 5.5 --- src/generics/lib.generics.ts | 18 ++++++++++++------ tests/weakref_basic.ts | 24 +++++++++++++++++++----- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/generics/lib.generics.ts b/src/generics/lib.generics.ts index f59534c..b3b9b59 100644 --- a/src/generics/lib.generics.ts +++ b/src/generics/lib.generics.ts @@ -1504,18 +1504,24 @@ namespace __Iterator } } +// gc only: the collector clears the link. Under any other model the compiler rejects the +// GC_* call below. class WeakRef { - private _target: Opaque | null; + // The target's address complemented, as gc.h's GC_HIDE_POINTER does: a plain pointer here is + // one the collector finds when it scans this object, and it then keeps the target alive, so + // the link was never cleared. The collector sets this to 0 when the target goes. + private _hidden: index; constructor(target: T) { - this._target = target; - GC_general_register_disappearing_link(Ref(this._target), target); + this._hidden = ~(target); + GC_general_register_disappearing_link(>Ref(this._hidden), target); } deref(): T | undefined { - if (this._target != null) { - return this._target; + const hidden = this._hidden; + if (hidden == 0) { + return undefined; } - return undefined; + return (~hidden); } } \ No newline at end of file diff --git a/tests/weakref_basic.ts b/tests/weakref_basic.ts index b550640..f2d5dd8 100644 --- a/tests/weakref_basic.ts +++ b/tests/weakref_basic.ts @@ -1,3 +1,5 @@ +// gc only: WeakRef is built on the collector's weak links, and this calls GC_gcollect() itself. +// Under any other model the compiler rejects both. class Foo { constructor(public value: int) { } @@ -9,17 +11,29 @@ function makeAndDrop(): WeakRef { return ref; } +// in a function of its own, so no pointer to the object stays in main's frame for the +// collector's conservative stack scan to find +function aliveValue(ref: WeakRef): int { + const target = ref.deref(); + return target != undefined ? target.value : -1; +} + function main() { const ref = makeAndDrop(); - // object should still be alive here since no GC ran yet in a way that could collect it - const before = ref.deref(); - console.log(before != undefined); + // nothing has collected yet + assert(aliveValue(ref) == 42, "deref before a collection gives the object"); + + // one that is still held + const kept = new Foo(7); + const keptRef = new WeakRef(kept); GC_gcollect(); GC_gcollect(); - const after = ref.deref(); - console.log(after == undefined); + assert(ref.deref() == undefined, "deref after the last reference went and a collection ran"); + assert(aliveValue(keptRef) == 7, "a held object survives the collection"); + assert(kept.value == 7); + console.log("ALL DONE"); }