objects now work as private non enumerable keys
This commit is contained in:
119
source/quickjs.c
119
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);
|
||||
}
|
||||
|
||||
|
||||
113
tests/suite.cm
113
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"
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user