reworked weak references so that cycles are (hopefully) correctly handled - added Symbol as WeakMap key, WeakRef and FinalizationRegistry

This commit is contained in:
Fabrice Bellard 2025-04-05 12:49:29 +02:00
parent bf164d640f
commit 8b5b1277ad
7 changed files with 550 additions and 121 deletions

2
TODO
View File

@ -62,6 +62,6 @@ Optimization ideas:
Test262o: 0/11262 errors, 463 excluded Test262o: 0/11262 errors, 463 excluded
Test262o commit: 7da91bceb9ce7613f87db47ddd1292a2dda58b42 (es5-tests branch) Test262o commit: 7da91bceb9ce7613f87db47ddd1292a2dda58b42 (es5-tests branch)
Result: 39/76768 errors, 3147 excluded, 7010 skipped Result: 39/76964 errors, 3147 excluded, 6912 skipped
Test262 commit: 56e77d6325067a545ea7e8ff5be5d9284334e33c Test262 commit: 56e77d6325067a545ea7e8ff5be5d9284334e33c

View File

@ -258,10 +258,6 @@ The following features are not supported yet:
@item Tail calls@footnote{We believe the current specification of tails calls is too complicated and presents limited practical interests.} @item Tail calls@footnote{We believe the current specification of tails calls is too complicated and presents limited practical interests.}
@item WeakRef and FinalizationRegistry objects
@item Symbols as WeakMap keys
@end itemize @end itemize
@subsection ECMA402 @subsection ECMA402

View File

@ -210,6 +210,8 @@ DEF(Float32Array, "Float32Array")
DEF(Float64Array, "Float64Array") DEF(Float64Array, "Float64Array")
DEF(DataView, "DataView") DEF(DataView, "DataView")
DEF(BigInt, "BigInt") DEF(BigInt, "BigInt")
DEF(WeakRef, "WeakRef")
DEF(FinalizationRegistry, "FinalizationRegistry")
DEF(Map, "Map") DEF(Map, "Map")
DEF(Set, "Set") /* Map + 1 */ DEF(Set, "Set") /* Map + 1 */
DEF(WeakMap, "WeakMap") /* Map + 2 */ DEF(WeakMap, "WeakMap") /* Map + 2 */

602
quickjs.c
View File

@ -171,7 +171,9 @@ enum {
JS_CLASS_ASYNC_FROM_SYNC_ITERATOR, /* u.async_from_sync_iterator_data */ JS_CLASS_ASYNC_FROM_SYNC_ITERATOR, /* u.async_from_sync_iterator_data */
JS_CLASS_ASYNC_GENERATOR_FUNCTION, /* u.func */ JS_CLASS_ASYNC_GENERATOR_FUNCTION, /* u.func */
JS_CLASS_ASYNC_GENERATOR, /* u.async_generator_data */ JS_CLASS_ASYNC_GENERATOR, /* u.async_generator_data */
JS_CLASS_WEAK_REF,
JS_CLASS_FINALIZATION_REGISTRY,
JS_CLASS_INIT_COUNT, /* last entry for predefined classes */ JS_CLASS_INIT_COUNT, /* last entry for predefined classes */
}; };
@ -251,6 +253,7 @@ struct JSRuntime {
struct list_head tmp_obj_list; /* used during GC */ struct list_head tmp_obj_list; /* used during GC */
JSGCPhaseEnum gc_phase : 8; JSGCPhaseEnum gc_phase : 8;
size_t malloc_gc_threshold; size_t malloc_gc_threshold;
struct list_head weakref_list; /* list of JSWeakRefHeader.link */
#ifdef DUMP_LEAKS #ifdef DUMP_LEAKS
struct list_head string_list; /* list of JSString.link */ struct list_head string_list; /* list of JSString.link */
#endif #endif
@ -342,6 +345,17 @@ struct JSGCObjectHeader {
struct list_head link; struct list_head link;
}; };
typedef enum {
JS_WEAKREF_TYPE_MAP,
JS_WEAKREF_TYPE_WEAKREF,
JS_WEAKREF_TYPE_FINREC,
} JSWeakRefHeaderTypeEnum;
typedef struct {
struct list_head link;
JSWeakRefHeaderTypeEnum weakref_type;
} JSWeakRefHeader;
typedef struct JSVarRef { typedef struct JSVarRef {
union { union {
JSGCObjectHeader header; /* must come first */ JSGCObjectHeader header; /* must come first */
@ -467,11 +481,6 @@ enum {
JS_ATOM_TYPE_PRIVATE, JS_ATOM_TYPE_PRIVATE,
}; };
enum {
JS_ATOM_HASH_SYMBOL,
JS_ATOM_HASH_PRIVATE,
};
typedef enum { typedef enum {
JS_ATOM_KIND_STRING, JS_ATOM_KIND_STRING,
JS_ATOM_KIND_SYMBOL, JS_ATOM_KIND_SYMBOL,
@ -479,13 +488,14 @@ typedef enum {
} JSAtomKindEnum; } JSAtomKindEnum;
#define JS_ATOM_HASH_MASK ((1 << 30) - 1) #define JS_ATOM_HASH_MASK ((1 << 30) - 1)
#define JS_ATOM_HASH_PRIVATE JS_ATOM_HASH_MASK
struct JSString { struct JSString {
JSRefCountHeader header; /* must come first, 32-bit */ JSRefCountHeader header; /* must come first, 32-bit */
uint32_t len : 31; uint32_t len : 31;
uint8_t is_wide_char : 1; /* 0 = 8 bits, 1 = 16 bits characters */ uint8_t is_wide_char : 1; /* 0 = 8 bits, 1 = 16 bits characters */
/* for JS_ATOM_TYPE_SYMBOL: hash = 0, atom_type = 3, /* for JS_ATOM_TYPE_SYMBOL: hash = weakref_count, atom_type = 3,
for JS_ATOM_TYPE_PRIVATE: hash = 1, atom_type = 3 for JS_ATOM_TYPE_PRIVATE: hash = JS_ATOM_HASH_PRIVATE, atom_type = 3
XXX: could change encoding to have one more bit in hash */ XXX: could change encoding to have one more bit in hash */
uint32_t hash : 30; uint32_t hash : 30;
uint8_t atom_type : 2; /* != 0 if atom, JS_ATOM_TYPE_x */ uint8_t atom_type : 2; /* != 0 if atom, JS_ATOM_TYPE_x */
@ -907,12 +917,12 @@ struct JSObject {
uint16_t class_id; /* see JS_CLASS_x */ uint16_t class_id; /* see JS_CLASS_x */
}; };
}; };
/* byte offsets: 16/24 */ /* count the number of weak references to this object. The object
structure is freed only if header.ref_count = 0 and
weakref_count = 0 */
uint32_t weakref_count;
JSShape *shape; /* prototype and property names + flag */ JSShape *shape; /* prototype and property names + flag */
JSProperty *prop; /* array of properties */ JSProperty *prop; /* array of properties */
/* byte offsets: 24/40 */
struct JSMapRecord *first_weak_ref; /* XXX: use a bit and an external hash table? */
/* byte offsets: 28/48 */
union { union {
void *opaque; void *opaque;
struct JSBoundFunction *bound_function; /* JS_CLASS_BOUND_FUNCTION */ struct JSBoundFunction *bound_function; /* JS_CLASS_BOUND_FUNCTION */
@ -969,7 +979,6 @@ struct JSObject {
JSRegExp regexp; /* JS_CLASS_REGEXP: 8/16 bytes */ JSRegExp regexp; /* JS_CLASS_REGEXP: 8/16 bytes */
JSValue object_data; /* for JS_SetObjectData(): 8/16/16 bytes */ JSValue object_data; /* for JS_SetObjectData(): 8/16/16 bytes */
} u; } u;
/* byte sizes: 40/48/72 */
}; };
enum { enum {
@ -1153,7 +1162,6 @@ static int JS_CreateProperty(JSContext *ctx, JSObject *p,
int flags); int flags);
static int js_string_memcmp(const JSString *p1, int pos1, const JSString *p2, static int js_string_memcmp(const JSString *p1, int pos1, const JSString *p2,
int pos2, int len); int pos2, int len);
static void reset_weak_ref(JSRuntime *rt, JSObject *p);
static JSValue js_array_buffer_constructor3(JSContext *ctx, static JSValue js_array_buffer_constructor3(JSContext *ctx,
JSValueConst new_target, JSValueConst new_target,
uint64_t len, JSClassID class_id, uint64_t len, JSClassID class_id,
@ -1248,6 +1256,10 @@ static JSValue JS_InstantiateFunctionListItem2(JSContext *ctx, JSObject *p,
JSAtom atom, void *opaque); JSAtom atom, void *opaque);
static JSValue js_object_groupBy(JSContext *ctx, JSValueConst this_val, static JSValue js_object_groupBy(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv, int is_map); int argc, JSValueConst *argv, int is_map);
static void map_delete_weakrefs(JSRuntime *rt, JSWeakRefHeader *wh);
static void weakref_delete_weakref(JSRuntime *rt, JSWeakRefHeader *wh);
static void finrec_delete_weakref(JSRuntime *rt, JSWeakRefHeader *wh);
static void JS_RunGCInternal(JSRuntime *rt, BOOL remove_weak_objects);
static const JSClassExoticMethods js_arguments_exotic_methods; static const JSClassExoticMethods js_arguments_exotic_methods;
static const JSClassExoticMethods js_string_exotic_methods; static const JSClassExoticMethods js_string_exotic_methods;
@ -1548,6 +1560,7 @@ JSRuntime *JS_NewRuntime2(const JSMallocFunctions *mf, void *opaque)
init_list_head(&rt->gc_obj_list); init_list_head(&rt->gc_obj_list);
init_list_head(&rt->gc_zero_ref_count_list); init_list_head(&rt->gc_zero_ref_count_list);
rt->gc_phase = JS_GC_PHASE_NONE; rt->gc_phase = JS_GC_PHASE_NONE;
init_list_head(&rt->weakref_list);
#ifdef DUMP_LEAKS #ifdef DUMP_LEAKS
init_list_head(&rt->string_list); init_list_head(&rt->string_list);
@ -1848,7 +1861,9 @@ void JS_FreeRuntime(JSRuntime *rt)
} }
init_list_head(&rt->job_list); init_list_head(&rt->job_list);
JS_RunGC(rt); /* don't remove the weak objects to avoid create new jobs with
FinalizationRegistry */
JS_RunGCInternal(rt, FALSE);
#ifdef DUMP_LEAKS #ifdef DUMP_LEAKS
/* leaking objects */ /* leaking objects */
@ -1890,6 +1905,7 @@ void JS_FreeRuntime(JSRuntime *rt)
} }
#endif #endif
assert(list_empty(&rt->gc_obj_list)); assert(list_empty(&rt->gc_obj_list));
assert(list_empty(&rt->weakref_list));
/* free the classes */ /* free the classes */
for(i = 0; i < rt->class_count; i++) { for(i = 0; i < rt->class_count; i++) {
@ -1934,7 +1950,7 @@ void JS_FreeRuntime(JSRuntime *rt)
printf(")"); printf(")");
break; break;
case JS_ATOM_TYPE_SYMBOL: case JS_ATOM_TYPE_SYMBOL:
if (p->hash == JS_ATOM_HASH_SYMBOL) { if (p->hash != JS_ATOM_HASH_PRIVATE) {
printf("Symbol("); printf("Symbol(");
JS_DumpString(rt, p); JS_DumpString(rt, p);
printf(")"); printf(")");
@ -2065,6 +2081,7 @@ JSContext *JS_NewContext(JSRuntime *rt)
JS_AddIntrinsicMapSet(ctx); JS_AddIntrinsicMapSet(ctx);
JS_AddIntrinsicTypedArrays(ctx); JS_AddIntrinsicTypedArrays(ctx);
JS_AddIntrinsicPromise(ctx); JS_AddIntrinsicPromise(ctx);
JS_AddIntrinsicWeakRef(ctx);
return ctx; return ctx;
} }
@ -2542,14 +2559,10 @@ static JSAtomKindEnum JS_AtomGetKind(JSContext *ctx, JSAtom v)
case JS_ATOM_TYPE_GLOBAL_SYMBOL: case JS_ATOM_TYPE_GLOBAL_SYMBOL:
return JS_ATOM_KIND_SYMBOL; return JS_ATOM_KIND_SYMBOL;
case JS_ATOM_TYPE_SYMBOL: case JS_ATOM_TYPE_SYMBOL:
switch(p->hash) { if (p->hash == JS_ATOM_HASH_PRIVATE)
case JS_ATOM_HASH_SYMBOL:
return JS_ATOM_KIND_SYMBOL;
case JS_ATOM_HASH_PRIVATE:
return JS_ATOM_KIND_PRIVATE; return JS_ATOM_KIND_PRIVATE;
default: else
abort(); return JS_ATOM_KIND_SYMBOL;
}
default: default:
abort(); abort();
} }
@ -2619,7 +2632,7 @@ static JSAtom __JS_NewAtom(JSRuntime *rt, JSString *str, int atom_type)
} else { } else {
h1 = 0; /* avoid warning */ h1 = 0; /* avoid warning */
if (atom_type == JS_ATOM_TYPE_SYMBOL) { if (atom_type == JS_ATOM_TYPE_SYMBOL) {
h = JS_ATOM_HASH_SYMBOL; h = 0;
} else { } else {
h = JS_ATOM_HASH_PRIVATE; h = JS_ATOM_HASH_PRIVATE;
atom_type = JS_ATOM_TYPE_SYMBOL; atom_type = JS_ATOM_TYPE_SYMBOL;
@ -2812,7 +2825,13 @@ static void JS_FreeAtomStruct(JSRuntime *rt, JSAtomStruct *p)
#ifdef DUMP_LEAKS #ifdef DUMP_LEAKS
list_del(&p->link); list_del(&p->link);
#endif #endif
js_free_rt(rt, p); if (p->atom_type == JS_ATOM_TYPE_SYMBOL &&
p->hash != JS_ATOM_HASH_PRIVATE && p->hash != 0) {
/* live weak references are still present on this object: keep
it */
} else {
js_free_rt(rt, p);
}
rt->atom_count--; rt->atom_count--;
assert(rt->atom_count >= 0); assert(rt->atom_count >= 0);
} }
@ -3157,7 +3176,7 @@ static BOOL JS_AtomSymbolHasDescription(JSContext *ctx, JSAtom v)
return FALSE; return FALSE;
p = rt->atom_array[v]; p = rt->atom_array[v];
return (((p->atom_type == JS_ATOM_TYPE_SYMBOL && return (((p->atom_type == JS_ATOM_TYPE_SYMBOL &&
p->hash == JS_ATOM_HASH_SYMBOL) || p->hash != JS_ATOM_HASH_PRIVATE) ||
p->atom_type == JS_ATOM_TYPE_GLOBAL_SYMBOL) && p->atom_type == JS_ATOM_TYPE_GLOBAL_SYMBOL) &&
!(p->len == 0 && p->is_wide_char != 0)); !(p->len == 0 && p->is_wide_char != 0));
} }
@ -5045,7 +5064,7 @@ static JSValue JS_NewObjectFromShape(JSContext *ctx, JSShape *sh, JSClassID clas
p->is_uncatchable_error = 0; p->is_uncatchable_error = 0;
p->tmp_mark = 0; p->tmp_mark = 0;
p->is_HTMLDDA = 0; p->is_HTMLDDA = 0;
p->first_weak_ref = NULL; p->weakref_count = 0;
p->u.opaque = NULL; p->u.opaque = NULL;
p->shape = sh; p->shape = sh;
p->prop = js_malloc(ctx, sizeof(JSProperty) * sh->prop_size); p->prop = js_malloc(ctx, sizeof(JSProperty) * sh->prop_size);
@ -5723,10 +5742,6 @@ static void free_object(JSRuntime *rt, JSObject *p)
p->shape = NULL; p->shape = NULL;
p->prop = NULL; p->prop = NULL;
if (unlikely(p->first_weak_ref)) {
reset_weak_ref(rt, p);
}
finalizer = rt->class_array[p->class_id].finalizer; finalizer = rt->class_array[p->class_id].finalizer;
if (finalizer) if (finalizer)
(*finalizer)(rt, JS_MKPTR(JS_TAG_OBJECT, p)); (*finalizer)(rt, JS_MKPTR(JS_TAG_OBJECT, p));
@ -5738,10 +5753,19 @@ static void free_object(JSRuntime *rt, JSObject *p)
p->u.func.home_object = NULL; p->u.func.home_object = NULL;
remove_gc_object(&p->header); remove_gc_object(&p->header);
if (rt->gc_phase == JS_GC_PHASE_REMOVE_CYCLES && p->header.ref_count != 0) { if (rt->gc_phase == JS_GC_PHASE_REMOVE_CYCLES) {
list_add_tail(&p->header.link, &rt->gc_zero_ref_count_list); if (p->header.ref_count == 0 && p->weakref_count == 0) {
js_free_rt(rt, p);
} else {
/* keep the object structure because there are may be
references to it */
list_add_tail(&p->header.link, &rt->gc_zero_ref_count_list);
}
} else { } else {
js_free_rt(rt, p); /* keep the object structure in case there are weak references to it */
if (p->weakref_count == 0) {
js_free_rt(rt, p);
}
} }
} }
@ -5859,6 +5883,36 @@ void __JS_FreeValue(JSContext *ctx, JSValue v)
/* garbage collection */ /* garbage collection */
static void gc_remove_weak_objects(JSRuntime *rt)
{
struct list_head *el;
/* add the freed objects to rt->gc_zero_ref_count_list so that
rt->weakref_list is not modified while we traverse it */
rt->gc_phase = JS_GC_PHASE_DECREF;
list_for_each(el, &rt->weakref_list) {
JSWeakRefHeader *wh = list_entry(el, JSWeakRefHeader, link);
switch(wh->weakref_type) {
case JS_WEAKREF_TYPE_MAP:
map_delete_weakrefs(rt, wh);
break;
case JS_WEAKREF_TYPE_WEAKREF:
weakref_delete_weakref(rt, wh);
break;
case JS_WEAKREF_TYPE_FINREC:
finrec_delete_weakref(rt, wh);
break;
default:
abort();
}
}
rt->gc_phase = JS_GC_PHASE_NONE;
/* free the freed objects here. */
free_zero_refcount(rt);
}
static void add_gc_object(JSRuntime *rt, JSGCObjectHeader *h, static void add_gc_object(JSRuntime *rt, JSGCObjectHeader *h,
JSGCObjectTypeEnum type) JSGCObjectTypeEnum type)
{ {
@ -6109,14 +6163,27 @@ static void gc_free_cycles(JSRuntime *rt)
assert(p->gc_obj_type == JS_GC_OBJ_TYPE_JS_OBJECT || assert(p->gc_obj_type == JS_GC_OBJ_TYPE_JS_OBJECT ||
p->gc_obj_type == JS_GC_OBJ_TYPE_FUNCTION_BYTECODE || p->gc_obj_type == JS_GC_OBJ_TYPE_FUNCTION_BYTECODE ||
p->gc_obj_type == JS_GC_OBJ_TYPE_ASYNC_FUNCTION); p->gc_obj_type == JS_GC_OBJ_TYPE_ASYNC_FUNCTION);
js_free_rt(rt, p); if (p->gc_obj_type == JS_GC_OBJ_TYPE_JS_OBJECT &&
((JSObject *)p)->weakref_count != 0) {
/* keep the object because there are weak references to it */
p->mark = 0;
} else {
js_free_rt(rt, p);
}
} }
init_list_head(&rt->gc_zero_ref_count_list); init_list_head(&rt->gc_zero_ref_count_list);
} }
void JS_RunGC(JSRuntime *rt) static void JS_RunGCInternal(JSRuntime *rt, BOOL remove_weak_objects)
{ {
if (remove_weak_objects) {
/* free the weakly referenced object or symbol structures, delete
the associated Map/Set entries and queue the finalization
registry callbacks. */
gc_remove_weak_objects(rt);
}
/* decrement the reference of the children of each object. mark = /* decrement the reference of the children of each object. mark =
1 after this pass. */ 1 after this pass. */
gc_decref(rt); gc_decref(rt);
@ -6128,6 +6195,11 @@ void JS_RunGC(JSRuntime *rt)
gc_free_cycles(rt); gc_free_cycles(rt);
} }
void JS_RunGC(JSRuntime *rt)
{
JS_RunGCInternal(rt, TRUE);
}
/* Return false if not an object or if the object has already been /* Return false if not an object or if the object has already been
freed (zombie objects are visible in finalizers when freeing freed (zombie objects are visible in finalizers when freeing
cycles). */ cycles). */
@ -46481,9 +46553,7 @@ static const JSCFunctionListEntry js_symbol_funcs[] = {
typedef struct JSMapRecord { typedef struct JSMapRecord {
int ref_count; /* used during enumeration to avoid freeing the record */ int ref_count; /* used during enumeration to avoid freeing the record */
BOOL empty; /* TRUE if the record is deleted */ BOOL empty : 8; /* TRUE if the record is deleted */
struct JSMapState *map;
struct JSMapRecord *next_weak_ref;
struct list_head link; struct list_head link;
struct JSMapRecord *hash_next; struct JSMapRecord *hash_next;
JSValue key; JSValue key;
@ -46498,8 +46568,82 @@ typedef struct JSMapState {
uint32_t hash_size; /* must be a power of two */ uint32_t hash_size; /* must be a power of two */
uint32_t record_count_threshold; /* count at which a hash table uint32_t record_count_threshold; /* count at which a hash table
resize is needed */ resize is needed */
JSWeakRefHeader weakref_header; /* only used if is_weak = TRUE */
} JSMapState; } JSMapState;
static BOOL js_weakref_is_target(JSValueConst val)
{
switch (JS_VALUE_GET_TAG(val)) {
case JS_TAG_OBJECT:
return TRUE;
case JS_TAG_SYMBOL:
{
JSAtomStruct *p = JS_VALUE_GET_PTR(val);
if (p->atom_type == JS_ATOM_TYPE_SYMBOL &&
p->hash != JS_ATOM_HASH_PRIVATE)
return TRUE;
}
break;
default:
break;
}
return FALSE;
}
/* JS_UNDEFINED is considered as a live weakref */
static BOOL js_weakref_is_live(JSValueConst val)
{
int *pref_count;
if (JS_IsUndefined(val))
return TRUE;
pref_count = JS_VALUE_GET_PTR(val);
return (*pref_count != 0);
}
/* 'val' can be JS_UNDEFINED */
static void js_weakref_free(JSRuntime *rt, JSValue val)
{
if (JS_VALUE_GET_TAG(val) == JS_TAG_OBJECT) {
JSObject *p = JS_VALUE_GET_OBJ(val);
assert(p->weakref_count >= 1);
p->weakref_count--;
if (p->weakref_count == 0 && p->header.ref_count == 0) {
if (rt->gc_phase == JS_GC_PHASE_REMOVE_CYCLES && p->header.mark) {
/* cannot remove now the structure if it is involved in a cycle */
} else {
/* can remove the dummy structure */
js_free_rt(rt, p);
}
}
} else if (JS_VALUE_GET_TAG(val) == JS_TAG_SYMBOL) {
JSString *p = JS_VALUE_GET_STRING(val);
assert(p->hash >= 1);
p->hash--;
if (p->hash == 0 && p->header.ref_count == 0) {
/* can remove the dummy structure */
js_free_rt(rt, p);
}
}
}
/* val must be an object, a symbol or undefined (see
js_weakref_is_target). */
static JSValue js_weakref_new(JSContext *ctx, JSValueConst val)
{
if (JS_VALUE_GET_TAG(val) == JS_TAG_OBJECT) {
JSObject *p = JS_VALUE_GET_OBJ(val);
p->weakref_count++;
} else if (JS_VALUE_GET_TAG(val) == JS_TAG_SYMBOL) {
JSString *p = JS_VALUE_GET_STRING(val);
/* XXX: could return an exception if too many references */
assert(p->hash < JS_ATOM_HASH_MASK - 2);
p->hash++;
} else {
assert(JS_IsUndefined(val));
}
return (JSValue)val;
}
#define MAGIC_SET (1 << 0) #define MAGIC_SET (1 << 0)
#define MAGIC_WEAK (1 << 1) #define MAGIC_WEAK (1 << 1)
@ -46521,6 +46665,10 @@ static JSValue js_map_constructor(JSContext *ctx, JSValueConst new_target,
goto fail; goto fail;
init_list_head(&s->records); init_list_head(&s->records);
s->is_weak = is_weak; s->is_weak = is_weak;
if (is_weak) {
s->weakref_header.weakref_type = JS_WEAKREF_TYPE_MAP;
list_add_tail(&s->weakref_header.link, &ctx->rt->weakref_list);
}
JS_SetOpaque(obj, s); JS_SetOpaque(obj, s);
s->hash_size = 1; s->hash_size = 1;
s->hash_table = js_mallocz(ctx, sizeof(s->hash_table[0]) * s->hash_size); s->hash_table = js_mallocz(ctx, sizeof(s->hash_table[0]) * s->hash_size);
@ -46687,8 +46835,12 @@ static JSMapRecord *map_find_record(JSContext *ctx, JSMapState *s,
uint32_t h; uint32_t h;
h = map_hash_key(key) & (s->hash_size - 1); h = map_hash_key(key) & (s->hash_size - 1);
for(mr = s->hash_table[h]; mr != NULL; mr = mr->hash_next) { for(mr = s->hash_table[h]; mr != NULL; mr = mr->hash_next) {
if (js_same_value_zero(ctx, mr->key, key)) if (mr->empty || (s->is_weak && !js_weakref_is_live(mr->key))) {
return mr; /* cannot match */
} else {
if (js_same_value_zero(ctx, mr->key, key))
return mr;
}
} }
return NULL; return NULL;
} }
@ -46714,7 +46866,8 @@ static void map_hash_resize(JSContext *ctx, JSMapState *s)
list_for_each(el, &s->records) { list_for_each(el, &s->records) {
mr = list_entry(el, JSMapRecord, link); mr = list_entry(el, JSMapRecord, link);
if (!mr->empty) { if (mr->empty || (s->is_weak && !js_weakref_is_live(mr->key))) {
} else {
h = map_hash_key(mr->key) & (new_hash_size - 1); h = map_hash_key(mr->key) & (new_hash_size - 1);
mr->hash_next = new_hash_table[h]; mr->hash_next = new_hash_table[h];
new_hash_table[h] = mr; new_hash_table[h] = mr;
@ -46735,17 +46888,12 @@ static JSMapRecord *map_add_record(JSContext *ctx, JSMapState *s,
if (!mr) if (!mr)
return NULL; return NULL;
mr->ref_count = 1; mr->ref_count = 1;
mr->map = s;
mr->empty = FALSE; mr->empty = FALSE;
if (s->is_weak) { if (s->is_weak) {
JSObject *p = JS_VALUE_GET_OBJ(key); mr->key = js_weakref_new(ctx, key);
/* Add the weak reference */
mr->next_weak_ref = p->first_weak_ref;
p->first_weak_ref = mr;
} else { } else {
JS_DupValue(ctx, key); mr->key = JS_DupValue(ctx, key);
} }
mr->key = (JSValue)key;
h = map_hash_key(key) & (s->hash_size - 1); h = map_hash_key(key) & (s->hash_size - 1);
mr->hash_next = s->hash_table[h]; mr->hash_next = s->hash_table[h];
s->hash_table[h] = mr; s->hash_table[h] = mr;
@ -46757,27 +46905,6 @@ static JSMapRecord *map_add_record(JSContext *ctx, JSMapState *s,
return mr; return mr;
} }
/* Remove the weak reference from the object weak
reference list. we don't use a doubly linked list to
save space, assuming a given object has few weak
references to it */
static void delete_weak_ref(JSRuntime *rt, JSMapRecord *mr)
{
JSMapRecord **pmr, *mr1;
JSObject *p;
p = JS_VALUE_GET_OBJ(mr->key);
pmr = &p->first_weak_ref;
for(;;) {
mr1 = *pmr;
assert(mr1 != NULL);
if (mr1 == mr)
break;
pmr = &mr1->next_weak_ref;
}
*pmr = mr1->next_weak_ref;
}
/* warning: the record must be removed from the hash table before */ /* warning: the record must be removed from the hash table before */
static void map_delete_record(JSRuntime *rt, JSMapState *s, JSMapRecord *mr) static void map_delete_record(JSRuntime *rt, JSMapState *s, JSMapRecord *mr)
{ {
@ -46785,7 +46912,7 @@ static void map_delete_record(JSRuntime *rt, JSMapState *s, JSMapRecord *mr)
return; return;
if (s->is_weak) { if (s->is_weak) {
delete_weak_ref(rt, mr); js_weakref_free(rt, mr->key);
} else { } else {
JS_FreeValueRT(rt, mr->key); JS_FreeValueRT(rt, mr->key);
} }
@ -46812,45 +46939,17 @@ static void map_decref_record(JSRuntime *rt, JSMapRecord *mr)
} }
} }
static void reset_weak_ref(JSRuntime *rt, JSObject *p) static void map_delete_weakrefs(JSRuntime *rt, JSWeakRefHeader *wh)
{ {
JSMapRecord *mr, *mr_next, **pmr, *mr1; JSMapState *s = container_of(wh, JSMapState, weakref_header);
JSMapState *s; struct list_head *el, *el1;
uint32_t h;
JSValue key;
/* first pass to remove the records from the WeakMap/WeakSet
lists */
key = JS_MKPTR(JS_TAG_OBJECT, p);
for(mr = p->first_weak_ref; mr != NULL; mr = mr->next_weak_ref) {
s = mr->map;
assert(s->is_weak);
assert(!mr->empty); /* no iterator on WeakMap/WeakSet */
/* remove the record from hash table */ list_for_each_safe(el, el1, &s->records) {
h = map_hash_key(key) & (s->hash_size - 1); JSMapRecord *mr = list_entry(el, JSMapRecord, link);
pmr = &s->hash_table[h]; if (!js_weakref_is_live(mr->key)) {
for(;;) { map_delete_record(rt, s, mr);
mr1 = *pmr;
assert(mr1 != NULL);
if (mr1 == mr)
break;
pmr = &mr1->hash_next;
} }
*pmr = mr->hash_next;
list_del(&mr->link);
} }
/* second pass to free the values to avoid modifying the weak
reference list while traversing it. */
for(mr = p->first_weak_ref; mr != NULL; mr = mr_next) {
mr_next = mr->next_weak_ref;
JS_FreeValueRT(rt, mr->value);
js_free_rt(rt, mr);
}
p->first_weak_ref = NULL; /* fail safe */
} }
static JSValue js_map_set(JSContext *ctx, JSValueConst this_val, static JSValue js_map_set(JSContext *ctx, JSValueConst this_val,
@ -46863,8 +46962,8 @@ static JSValue js_map_set(JSContext *ctx, JSValueConst this_val,
if (!s) if (!s)
return JS_EXCEPTION; return JS_EXCEPTION;
key = map_normalize_key(ctx, argv[0]); key = map_normalize_key(ctx, argv[0]);
if (s->is_weak && !JS_IsObject(key)) if (s->is_weak && !js_weakref_is_target(key))
return JS_ThrowTypeErrorNotAnObject(ctx); return JS_ThrowTypeError(ctx, "invalid value used as %s key", (magic & MAGIC_SET) ? "WeakSet" : "WeakMap");
if (magic & MAGIC_SET) if (magic & MAGIC_SET)
value = JS_UNDEFINED; value = JS_UNDEFINED;
else else
@ -46930,8 +47029,12 @@ static JSValue js_map_delete(JSContext *ctx, JSValueConst this_val,
mr = *pmr; mr = *pmr;
if (mr == NULL) if (mr == NULL)
return JS_FALSE; return JS_FALSE;
if (js_same_value_zero(ctx, mr->key, key)) if (mr->empty || (s->is_weak && !js_weakref_is_live(mr->key))) {
break; /* not valid */
} else {
if (js_same_value_zero(ctx, mr->key, key))
break;
}
pmr = &mr->hash_next; pmr = &mr->hash_next;
} }
@ -47151,7 +47254,7 @@ static void js_map_finalizer(JSRuntime *rt, JSValue val)
mr = list_entry(el, JSMapRecord, link); mr = list_entry(el, JSMapRecord, link);
if (!mr->empty) { if (!mr->empty) {
if (s->is_weak) if (s->is_weak)
delete_weak_ref(rt, mr); js_weakref_free(rt, mr->key);
else else
JS_FreeValueRT(rt, mr->key); JS_FreeValueRT(rt, mr->key);
JS_FreeValueRT(rt, mr->value); JS_FreeValueRT(rt, mr->value);
@ -47159,6 +47262,9 @@ static void js_map_finalizer(JSRuntime *rt, JSValue val)
js_free_rt(rt, mr); js_free_rt(rt, mr);
} }
js_free_rt(rt, s->hash_table); js_free_rt(rt, s->hash_table);
if (s->is_weak) {
list_del(&s->weakref_header.link);
}
js_free_rt(rt, s); js_free_rt(rt, s);
} }
} }
@ -53717,3 +53823,277 @@ void JS_AddIntrinsicTypedArrays(JSContext *ctx)
JS_AddIntrinsicAtomics(ctx); JS_AddIntrinsicAtomics(ctx);
#endif #endif
} }
/* WeakRef */
typedef struct JSWeakRefData {
JSWeakRefHeader weakref_header;
JSValue target;
} JSWeakRefData;
static void js_weakref_finalizer(JSRuntime *rt, JSValue val)
{
JSWeakRefData *wrd = JS_GetOpaque(val, JS_CLASS_WEAK_REF);
if (!wrd)
return;
js_weakref_free(rt, wrd->target);
list_del(&wrd->weakref_header.link);
js_free_rt(rt, wrd);
}
static void weakref_delete_weakref(JSRuntime *rt, JSWeakRefHeader *wh)
{
JSWeakRefData *wrd = container_of(wh, JSWeakRefData, weakref_header);
if (!js_weakref_is_live(wrd->target)) {
js_weakref_free(rt, wrd->target);
wrd->target = JS_UNDEFINED;
}
}
static JSValue js_weakref_constructor(JSContext *ctx, JSValueConst new_target,
int argc, JSValueConst *argv)
{
JSValueConst arg;
JSValue obj;
if (JS_IsUndefined(new_target))
return JS_ThrowTypeError(ctx, "constructor requires 'new'");
arg = argv[0];
if (!js_weakref_is_target(arg))
return JS_ThrowTypeError(ctx, "invalid target");
obj = js_create_from_ctor(ctx, new_target, JS_CLASS_WEAK_REF);
if (JS_IsException(obj))
return JS_EXCEPTION;
JSWeakRefData *wrd = js_mallocz(ctx, sizeof(*wrd));
if (!wrd) {
JS_FreeValue(ctx, obj);
return JS_EXCEPTION;
}
wrd->target = js_weakref_new(ctx, arg);
wrd->weakref_header.weakref_type = JS_WEAKREF_TYPE_WEAKREF;
list_add_tail(&wrd->weakref_header.link, &ctx->rt->weakref_list);
JS_SetOpaque(obj, wrd);
return obj;
}
static JSValue js_weakref_deref(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
JSWeakRefData *wrd = JS_GetOpaque2(ctx, this_val, JS_CLASS_WEAK_REF);
if (!wrd)
return JS_EXCEPTION;
return JS_DupValue(ctx, wrd->target);
}
static const JSCFunctionListEntry js_weakref_proto_funcs[] = {
JS_CFUNC_DEF("deref", 0, js_weakref_deref ),
JS_PROP_STRING_DEF("[Symbol.toStringTag]", "WeakRef", JS_PROP_CONFIGURABLE ),
};
static const JSClassShortDef js_weakref_class_def[] = {
{ JS_ATOM_WeakRef, js_weakref_finalizer, NULL }, /* JS_CLASS_WEAK_REF */
};
typedef struct JSFinRecEntry {
struct list_head link;
JSValue target;
JSValue held_val;
JSValue token;
} JSFinRecEntry;
typedef struct JSFinalizationRegistryData {
JSWeakRefHeader weakref_header;
struct list_head entries; /* list of JSFinRecEntry.link */
JSContext *ctx;
JSValue cb;
} JSFinalizationRegistryData;
static void js_finrec_finalizer(JSRuntime *rt, JSValue val)
{
JSFinalizationRegistryData *frd = JS_GetOpaque(val, JS_CLASS_FINALIZATION_REGISTRY);
if (frd) {
struct list_head *el, *el1;
list_for_each_safe(el, el1, &frd->entries) {
JSFinRecEntry *fre = list_entry(el, JSFinRecEntry, link);
js_weakref_free(rt, fre->target);
js_weakref_free(rt, fre->token);
JS_FreeValueRT(rt, fre->held_val);
js_free_rt(rt, fre);
}
JS_FreeValueRT(rt, frd->cb);
list_del(&frd->weakref_header.link);
js_free_rt(rt, frd);
}
}
static void js_finrec_mark(JSRuntime *rt, JSValueConst val,
JS_MarkFunc *mark_func)
{
JSFinalizationRegistryData *frd = JS_GetOpaque(val, JS_CLASS_FINALIZATION_REGISTRY);
struct list_head *el;
if (frd) {
list_for_each(el, &frd->entries) {
JSFinRecEntry *fre = list_entry(el, JSFinRecEntry, link);
JS_MarkValue(rt, fre->held_val, mark_func);
}
JS_MarkValue(rt, frd->cb, mark_func);
}
}
static JSValue js_finrec_job(JSContext *ctx, int argc, JSValueConst *argv)
{
return JS_Call(ctx, argv[0], JS_UNDEFINED, 1, &argv[1]);
}
static void finrec_delete_weakref(JSRuntime *rt, JSWeakRefHeader *wh)
{
JSFinalizationRegistryData *frd = container_of(wh, JSFinalizationRegistryData, weakref_header);
struct list_head *el, *el1;
list_for_each_safe(el, el1, &frd->entries) {
JSFinRecEntry *fre = list_entry(el, JSFinRecEntry, link);
if (!js_weakref_is_live(fre->token)) {
js_weakref_free(rt, fre->token);
fre->token = JS_UNDEFINED;
}
if (!js_weakref_is_live(fre->target)) {
JSValueConst args[2];
args[0] = frd->cb;
args[1] = fre->held_val;
JS_EnqueueJob(frd->ctx, js_finrec_job, 2, args);
js_weakref_free(rt, fre->target);
js_weakref_free(rt, fre->token);
JS_FreeValueRT(rt, fre->held_val);
list_del(&fre->link);
js_free_rt(rt, fre);
}
}
}
static JSValue js_finrec_constructor(JSContext *ctx, JSValueConst new_target,
int argc, JSValueConst *argv)
{
JSValueConst cb;
JSValue obj;
JSFinalizationRegistryData *frd;
if (JS_IsUndefined(new_target))
return JS_ThrowTypeError(ctx, "constructor requires 'new'");
cb = argv[0];
if (!JS_IsFunction(ctx, cb))
return JS_ThrowTypeError(ctx, "argument must be a function");
obj = js_create_from_ctor(ctx, new_target, JS_CLASS_FINALIZATION_REGISTRY);
if (JS_IsException(obj))
return JS_EXCEPTION;
frd = js_mallocz(ctx, sizeof(*frd));
if (!frd) {
JS_FreeValue(ctx, obj);
return JS_EXCEPTION;
}
frd->weakref_header.weakref_type = JS_WEAKREF_TYPE_FINREC;
list_add_tail(&frd->weakref_header.link, &ctx->rt->weakref_list);
init_list_head(&frd->entries);
frd->ctx = ctx; /* XXX: JS_DupContext() ? */
frd->cb = JS_DupValue(ctx, cb);
JS_SetOpaque(obj, frd);
return obj;
}
static JSValue js_finrec_register(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
JSValueConst target, held_val, token;
JSFinalizationRegistryData *frd;
JSFinRecEntry *fre;
frd = JS_GetOpaque2(ctx, this_val, JS_CLASS_FINALIZATION_REGISTRY);
if (!frd)
return JS_EXCEPTION;
target = argv[0];
held_val = argv[1];
token = argc > 2 ? argv[2] : JS_UNDEFINED;
if (!js_weakref_is_target(target))
return JS_ThrowTypeError(ctx, "invalid target");
if (js_same_value(ctx, target, held_val))
return JS_ThrowTypeError(ctx, "held value cannot be the target");
if (!JS_IsUndefined(token) && !js_weakref_is_target(token))
return JS_ThrowTypeError(ctx, "invalid unregister token");
fre = js_malloc(ctx, sizeof(*fre));
if (!fre)
return JS_EXCEPTION;
fre->target = js_weakref_new(ctx, target);
fre->held_val = JS_DupValue(ctx, held_val);
fre->token = js_weakref_new(ctx, token);
list_add_tail(&fre->link, &frd->entries);
return JS_UNDEFINED;
}
static JSValue js_finrec_unregister(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
JSFinalizationRegistryData *frd = JS_GetOpaque2(ctx, this_val, JS_CLASS_FINALIZATION_REGISTRY);
JSValueConst token;
BOOL removed;
struct list_head *el, *el1;
if (!frd)
return JS_EXCEPTION;
token = argv[0];
if (!js_weakref_is_target(token))
return JS_ThrowTypeError(ctx, "invalid unregister token");
removed = FALSE;
list_for_each_safe(el, el1, &frd->entries) {
JSFinRecEntry *fre = list_entry(el, JSFinRecEntry, link);
if (js_same_value(ctx, fre->token, token)) {
js_weakref_free(ctx->rt, fre->target);
js_weakref_free(ctx->rt, fre->token);
JS_FreeValue(ctx, fre->held_val);
list_del(&fre->link);
js_free(ctx, fre);
removed = TRUE;
}
}
return JS_NewBool(ctx, removed);
}
static const JSCFunctionListEntry js_finrec_proto_funcs[] = {
JS_CFUNC_DEF("register", 2, js_finrec_register ),
JS_CFUNC_DEF("unregister", 1, js_finrec_unregister ),
JS_PROP_STRING_DEF("[Symbol.toStringTag]", "FinalizationRegistry", JS_PROP_CONFIGURABLE ),
};
static const JSClassShortDef js_finrec_class_def[] = {
{ JS_ATOM_FinalizationRegistry, js_finrec_finalizer, js_finrec_mark }, /* JS_CLASS_FINALIZATION_REGISTRY */
};
void JS_AddIntrinsicWeakRef(JSContext *ctx)
{
JSRuntime *rt = ctx->rt;
/* WeakRef */
if (!JS_IsRegisteredClass(rt, JS_CLASS_WEAK_REF)) {
init_class_range(rt, js_weakref_class_def, JS_CLASS_WEAK_REF,
countof(js_weakref_class_def));
}
ctx->class_proto[JS_CLASS_WEAK_REF] = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_WEAK_REF],
js_weakref_proto_funcs,
countof(js_weakref_proto_funcs));
JS_NewGlobalCConstructor(ctx, "WeakRef", js_weakref_constructor, 1, ctx->class_proto[JS_CLASS_WEAK_REF]);
/* FinalizationRegistry */
if (!JS_IsRegisteredClass(rt, JS_CLASS_FINALIZATION_REGISTRY)) {
init_class_range(rt, js_finrec_class_def, JS_CLASS_FINALIZATION_REGISTRY,
countof(js_finrec_class_def));
}
ctx->class_proto[JS_CLASS_FINALIZATION_REGISTRY] = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_FINALIZATION_REGISTRY],
js_finrec_proto_funcs,
countof(js_finrec_proto_funcs));
JS_NewGlobalCConstructor(ctx, "FinalizationRegistry", js_finrec_constructor, 1, ctx->class_proto[JS_CLASS_FINALIZATION_REGISTRY]);
}

View File

@ -405,6 +405,7 @@ void JS_AddIntrinsicProxy(JSContext *ctx);
void JS_AddIntrinsicMapSet(JSContext *ctx); void JS_AddIntrinsicMapSet(JSContext *ctx);
void JS_AddIntrinsicTypedArrays(JSContext *ctx); void JS_AddIntrinsicTypedArrays(JSContext *ctx);
void JS_AddIntrinsicPromise(JSContext *ctx); void JS_AddIntrinsicPromise(JSContext *ctx);
void JS_AddIntrinsicWeakRef(JSContext *ctx);
JSValue js_string_codePointRange(JSContext *ctx, JSValueConst this_val, JSValue js_string_codePointRange(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv); int argc, JSValueConst *argv);

View File

@ -107,7 +107,7 @@ Error.isError=skip
explicit-resource-management=skip explicit-resource-management=skip
exponentiation exponentiation
export-star-as-namespace-from-module export-star-as-namespace-from-module
FinalizationRegistry=skip FinalizationRegistry
Float16Array=skip Float16Array=skip
Float32Array Float32Array
Float64Array Float64Array
@ -217,7 +217,7 @@ Symbol.split
Symbol.toPrimitive Symbol.toPrimitive
Symbol.toStringTag Symbol.toStringTag
Symbol.unscopables Symbol.unscopables
symbols-as-weakmap-keys=skip symbols-as-weakmap-keys
tail-call-optimization=skip tail-call-optimization=skip
template template
Temporal=skip Temporal=skip
@ -231,7 +231,7 @@ Uint8Array
uint8array-base64=skip uint8array-base64=skip
Uint8ClampedArray Uint8ClampedArray
WeakMap WeakMap
WeakRef=skip WeakRef
WeakSet WeakSet
well-formed-json-stringify well-formed-json-stringify

View File

@ -789,22 +789,70 @@ function test_weak_map()
tab = []; tab = [];
for(i = 0; i < n; i++) { for(i = 0; i < n; i++) {
v = { }; v = { };
o = { id: i }; if (i & 1)
o = Symbol("x" + i);
else
o = { id: i };
tab[i] = [o, v]; tab[i] = [o, v];
a.set(o, v); a.set(o, v);
} }
o = null; o = null;
n2 = n >> 1; n2 = 5;
for(i = 0; i < n2; i++) { for(i = 0; i < n2; i++) {
a.delete(tab[i][0]); a.delete(tab[i][0]);
} }
for(i = n2; i < n; i++) { for(i = n2; i < n; i++) {
tab[i][0] = null; /* should remove the object from the WeakMap too */ tab[i][0] = null; /* should remove the object from the WeakMap too */
} }
std.gc();
/* the WeakMap should be empty here */ /* the WeakMap should be empty here */
} }
function test_weak_ref()
{
var w1, w2, o, i;
for(i = 0; i < 2; i++) {
if (i == 0)
o = { };
else
o = Symbol("x");
w1 = new WeakRef(o);
assert(w1.deref(), o);
w2 = new WeakRef(o);
assert(w2.deref(), o);
o = null;
std.gc();
assert(w1.deref(), undefined);
assert(w2.deref(), undefined);
}
}
function test_finalization_registry()
{
{
let expected = {};
let actual;
let finrec = new FinalizationRegistry(v => { actual = v });
finrec.register({}, expected);
os.setTimeout(() => {
assert(actual, expected);
}, 0);
}
{
let expected = 42;
let actual;
let finrec = new FinalizationRegistry(v => { actual = v });
finrec.register({}, expected);
os.setTimeout(() => {
assert(actual, expected);
}, 0);
}
std.gc();
}
function test_generator() function test_generator()
{ {
function *f() { function *f() {
@ -905,5 +953,7 @@ test_regexp();
test_symbol(); test_symbol();
test_map(); test_map();
test_weak_map(); test_weak_map();
test_weak_ref();
test_finalization_registry();
test_generator(); test_generator();
test_rope(); test_rope();