From d12d77c22cea7a1fa757ea2053848087b3322980 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 2 Jan 2026 16:28:02 -0600 Subject: [PATCH] objects now work as private non enumerable keys --- source/quickjs.c | 119 +++++++++++++++++++++++++++++++++++++++++++++-- tests/suite.cm | 113 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+), 3 deletions(-) diff --git a/source/quickjs.c b/source/quickjs.c index 6c96c9cb..32533a42 100644 --- a/source/quickjs.c +++ b/source/quickjs.c @@ -500,6 +500,16 @@ struct JSString { } u; }; +/* Extended symbol atom structure with object-key payload */ +typedef struct JSAtomSymbol { + JSString s; /* base atom struct */ + JSValue obj_key; /* JS_UNDEFINED for normal symbols; strong ref for object-key symbols */ +} JSAtomSymbol; + +static inline JSAtomSymbol *js_atom_as_symbol(JSAtomStruct *p) { + return (JSAtomSymbol *)p; +} + typedef struct JSStringRope { JSRefCountHeader header; /* must come first, 32-bit */ uint32_t len; @@ -815,6 +825,7 @@ struct JSObject { }; JSShape *shape; /* prototype and property names + flag */ JSProperty *prop; /* array of properties */ + JSAtom object_key_atom; /* cached atom index for object-as-key (non-owning hint) */ union { void *opaque; struct JSBoundFunction *bound_function; /* JS_CLASS_BOUND_FUNCTION */ @@ -2508,12 +2519,16 @@ static JSAtom __JS_NewAtom(JSRuntime *rt, JSString *str, int atom_type) js_free_string(rt, str); } } else { - p = js_malloc_rt(rt, sizeof(JSAtomStruct)); /* empty wide string */ - if (!p) + /* Allocate extended JSAtomSymbol for symbol atoms */ + JSAtomSymbol *sp; + sp = js_malloc_rt(rt, sizeof(JSAtomSymbol)); + if (!sp) return JS_ATOM_NULL; + p = &sp->s; p->header.ref_count = 1; p->is_wide_char = 1; /* Hack to represent NULL as a JSString */ p->len = 0; + sp->obj_key = JS_NULL; #ifdef DUMP_LEAKS list_add_tail(&p->link, &rt->string_list); #endif @@ -2624,6 +2639,14 @@ static void JS_FreeAtomStruct(JSRuntime *rt, JSAtomStruct *p) /* live weak references are still present on this object: keep it */ } else { + /* Free object-key payload for symbol atoms before freeing struct */ + if (p->atom_type == JS_ATOM_TYPE_SYMBOL) { + JSAtomSymbol *sp = js_atom_as_symbol(p); + if (!JS_IsNull(sp->obj_key)) { + JS_FreeValueRT(rt, sp->obj_key); + sp->obj_key = JS_NULL; + } + } js_free_rt(rt, p); } rt->atom_count--; @@ -2640,6 +2663,70 @@ static void __JS_FreeAtom(JSRuntime *rt, uint32_t i) JS_FreeAtomStruct(rt, p); } +/* Get or create a unique symbol atom for using an object as a property key. + Returns JS_ATOM_NULL on allocation failure. */ +static JSAtom js_get_object_key_atom(JSContext *ctx, JSObject *key_obj) +{ + JSRuntime *rt = ctx->rt; + JSAtom atom = key_obj->object_key_atom; + + /* Validate cached atom (non-owning; may be stale) */ + if (atom != JS_ATOM_NULL && + atom < (JSAtom)rt->atom_size && + rt->atom_array[atom] != NULL && + !atom_is_free(rt->atom_array[atom])) { + + JSAtomStruct *ap = rt->atom_array[atom]; + if (ap->atom_type == JS_ATOM_TYPE_SYMBOL) { + JSAtomSymbol *sp = js_atom_as_symbol(ap); + + if (JS_VALUE_GET_TAG(sp->obj_key) == JS_TAG_OBJECT && + JS_VALUE_GET_OBJ(sp->obj_key) == key_obj) { + return atom; + } + } + } + + /* Create a fresh symbol atom (passing NULL for symbol str) */ + atom = __JS_NewAtom(rt, NULL, JS_ATOM_TYPE_SYMBOL); + if (atom == JS_ATOM_NULL) + return JS_ATOM_NULL; + + { + JSAtomStruct *ap = rt->atom_array[atom]; + JSAtomSymbol *sp = js_atom_as_symbol(ap); + sp->obj_key = JS_DupValue(ctx, JS_MKPTR(JS_TAG_OBJECT, key_obj)); + } + + key_obj->object_key_atom = atom; + return atom; +} + +/* Get or create a symbol value for using an object as a property key. + Returns JS_EXCEPTION on allocation failure. */ +static JSValue js_get_object_key_symbol(JSContext *ctx, JSObject *key_obj) +{ + JSAtom atom = js_get_object_key_atom(ctx, key_obj); + if (atom == JS_ATOM_NULL) + return JS_ThrowOutOfMemory(ctx); + + return JS_MKPTR(JS_TAG_SYMBOL, ctx->rt->atom_array[atom]); +} + +/* Check if a symbol atom is an object-key symbol (has obj_key payload) */ +static BOOL js_atom_is_object_key_symbol(JSRuntime *rt, JSAtom atom) +{ + if (__JS_AtomIsTaggedInt(atom)) + return FALSE; + if (atom >= (JSAtom)rt->atom_size) + return FALSE; + JSAtomStruct *ap = rt->atom_array[atom]; + if (!ap || ap->atom_type != JS_ATOM_TYPE_SYMBOL) + return FALSE; + JSAtomSymbol *sp = js_atom_as_symbol(ap); + return !JS_IsNull(sp->obj_key); +} + /* Warning: 'p' is freed */ static JSAtom JS_NewAtomStr(JSContext *ctx, JSString *p) { @@ -4736,6 +4823,7 @@ static JSValue JS_NewObjectFromShape(JSContext *ctx, JSShape *sh, JSClassID clas p->is_constructor = 0; p->has_immutable_prototype = 0; p->tmp_mark = 0; + p->object_key_atom = JS_ATOM_NULL; p->u.opaque = NULL; p->shape = sh; p->prop = js_malloc(ctx, sizeof(JSProperty) * sh->prop_size); @@ -5496,6 +5584,16 @@ static void mark_children(JSRuntime *rt, JSGCObjectHeader *gp, for(i = 0; i < sh->prop_count; i++) { JSProperty *pr = &p->prop[i]; if (prs->atom != JS_ATOM_NULL) { + /* Mark object-key symbol payload to keep key object alive */ + if (!__JS_AtomIsTaggedInt(prs->atom)) { + JSAtomStruct *ap = rt->atom_array[prs->atom]; + if (ap && ap->atom_type == JS_ATOM_TYPE_SYMBOL) { + JSAtomSymbol *sp = js_atom_as_symbol(ap); + if (!JS_IsNull(sp->obj_key)) { + JS_MarkValue(rt, sp->obj_key, mark_func); + } + } + } if (prs->flags & JS_PROP_TMASK) { if ((prs->flags & JS_PROP_TMASK) == JS_PROP_GETSET) { if (pr->u.getset.getter) @@ -7127,6 +7225,9 @@ static int __exception JS_GetOwnPropertyNamesInternal(JSContext *ctx, for(i = 0, prs = get_shape_prop(sh); i < sh->prop_count; i++, prs++) { atom = prs->atom; if (atom != JS_ATOM_NULL) { + /* Skip object-key symbols (private access tokens, not enumerable) */ + if (js_atom_is_object_key_symbol(ctx->rt, atom)) + continue; is_enumerable = ((prs->flags & JS_PROP_ENUMERABLE) != 0); kind = JS_AtomGetKind(ctx, atom); if ((!(flags & JS_GPN_ENUM_ONLY) || is_enumerable) && @@ -7197,6 +7298,9 @@ static int __exception JS_GetOwnPropertyNamesInternal(JSContext *ctx, for(i = 0, prs = get_shape_prop(sh); i < sh->prop_count; i++, prs++) { atom = prs->atom; if (atom != JS_ATOM_NULL) { + /* Skip object-key symbols (private access tokens, not enumerable) */ + if (js_atom_is_object_key_symbol(ctx->rt, atom)) + continue; is_enumerable = ((prs->flags & JS_PROP_ENUMERABLE) != 0); kind = JS_AtomGetKind(ctx, atom); if ((!(flags & JS_GPN_ENUM_ONLY) || is_enumerable) && @@ -7442,7 +7546,8 @@ JSAtom JS_ValueToAtom(JSContext *ctx, JSValueConst val) if (JS_IsException(str)) return JS_ATOM_NULL; if (JS_VALUE_GET_TAG(str) == JS_TAG_SYMBOL) { - atom = js_symbol_to_atom(ctx, str); + /* Must dup atom for proper ownership (especially for object-key symbols) */ + atom = JS_DupAtom(ctx, js_symbol_to_atom(ctx, str)); } else { atom = JS_NewAtomStr(ctx, JS_VALUE_GET_STRING(str)); } @@ -10170,6 +10275,14 @@ static JSValue JS_ToLocaleStringFree(JSContext *ctx, JSValue val) JSValue JS_ToPropertyKey(JSContext *ctx, JSValueConst val) { + int tag = JS_VALUE_GET_TAG(val); + + /* Objects become their cached object-key symbol (identity-based key) */ + if (tag == JS_TAG_OBJECT) { + return js_get_object_key_symbol(ctx, JS_VALUE_GET_OBJ(val)); + } + + /* Preserve existing behavior for everything else */ return JS_ToStringInternal(ctx, val, TRUE); } diff --git a/tests/suite.cm b/tests/suite.cm index a4b2e6d3..f835aab3 100644 --- a/tests/suite.cm +++ b/tests/suite.cm @@ -1609,4 +1609,117 @@ return { var nn = val.a if (nn != null) throw "val.a should return null" }, + + // ============================================================================ + // OBJECT-AS-KEY (Private Property Access) + // ============================================================================ + + test_object_key_basic: function() { + var k1 = {} + var k2 = {} + var o = {} + o[k1] = 123 + o[k2] = 456 + if (o[k1] != 123) throw "object key k1 failed" + if (o[k2] != 456) throw "object key k2 failed" + }, + + test_object_key_new_object_different_key: function() { + var k1 = {} + var o = {} + o[k1] = 123 + if (o[{}] != null) throw "new object should be different key" + }, + + test_object_key_in_operator: function() { + var k1 = {} + var o = {} + o[k1] = 123 + if (!(k1 in o)) throw "in operator should find object key" + }, + + test_object_key_delete: function() { + var k1 = {} + var o = {} + o[k1] = 123 + delete o[k1] + if ((k1 in o)) throw "delete should remove object key" + }, + + test_object_key_no_string_collision: function() { + var a = {} + var b = {} + var o = {} + o[a] = 1 + o[b] = 2 + if (o[a] != 1) throw "object key a should be 1" + if (o[b] != 2) throw "object key b should be 2" + }, + + test_object_key_same_object_same_key: function() { + var k = {} + var o = {} + o[k] = 100 + o[k] = 200 + if (o[k] != 200) throw "same object should be same key" + }, + + test_object_key_not_in_for_in: function() { + var k = {} + var o = {a: 1, b: 2} + o[k] = 999 + var count = 0 + for (var key in o) { + count = count + 1 + if (key == k) throw "object key should not appear in for-in" + } + if (count != 2) throw "for-in should only see string keys" + }, + + test_object_key_computed_property: function() { + var k = {} + var o = {} + o[k] = function() { return 42 } + if (o[k]() != 42) throw "object key with function value failed" + }, + + test_object_key_multiple_objects_multiple_keys: function() { + var k1 = {} + var k2 = {} + var k3 = {} + var o = {} + o[k1] = "one" + o[k2] = "two" + o[k3] = "three" + if (o[k1] != "one") throw "multiple keys k1 failed" + if (o[k2] != "two") throw "multiple keys k2 failed" + if (o[k3] != "three") throw "multiple keys k3 failed" + }, + + test_object_key_with_string_keys: function() { + var k = {} + var o = {name: "test"} + o[k] = "private" + if (o.name != "test") throw "string key should still work" + if (o[k] != "private") throw "object key should work with string keys" + }, + + test_object_key_overwrite: function() { + var k = {} + var o = {} + o[k] = 1 + o[k] = 2 + o[k] = 3 + if (o[k] != 3) throw "object key overwrite failed" + }, + + test_object_key_nested_objects: function() { + var k1 = {} + var k2 = {} + var inner = {} + inner[k2] = "nested" + var outer = {} + outer[k1] = inner + if (outer[k1][k2] != "nested") throw "nested object keys failed" + }, }