objects directly used as properties now instead of shadow symbol table

This commit is contained in:
2026-01-23 23:33:52 -06:00
parent 0d93741c31
commit d8b13548d2
2 changed files with 295 additions and 102 deletions

View File

@@ -602,15 +602,6 @@ struct JSString {
} u;
};
/* Extended symbol atom structure with object-key payload */
typedef struct JSAtomSymbol {
JSString s; /* base atom struct */
JSValue obj_key; /* JS_NULL for normal symbols; strong ref for object-key symbols */
} JSAtomSymbol;
static inline JSAtomSymbol *js_atom_as_symbol(JSAtomStruct *p) {
return (JSAtomSymbol *)p;
}
#ifdef RC_TRACE
static void rc_dump_atom(JSRuntime *rt, uint32_t atom)
@@ -656,9 +647,7 @@ static void rc_dump_atom(JSRuntime *rt, uint32_t atom)
}
fprintf(stderr, "\"\n");
} else if (p->atom_type == JS_ATOM_TYPE_SYMBOL) {
JSAtomSymbol *sp = js_atom_as_symbol(p);
fprintf(stderr, "type=symbol obj_key=%d\n",
JS_VALUE_GET_TAG(sp->obj_key) != JS_TAG_NULL);
fprintf(stderr, "type=symbol\n");
} else {
fprintf(stderr, "type=unknown(%u)\n", (unsigned)p->atom_type);
}
@@ -911,7 +900,10 @@ 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) */
/* Object-key map for using objects as property keys (identity-based) */
JSValue *objkey_tab; /* hash table: even indices are keys, odd are values */
uint32_t objkey_count; /* number of entries */
uint32_t objkey_size; /* allocated size (power of 2, 0 if none) */
union {
void *opaque;
struct JSBoundFunction *bound_function; /* JS_CLASS_BOUND_FUNCTION */
@@ -2531,16 +2523,13 @@ static JSAtom __JS_NewAtom(JSRuntime *rt, JSString *str, int atom_type)
js_free_string(rt, str);
}
} else {
/* Allocate extended JSAtomSymbol for symbol atoms */
JSAtomSymbol *sp;
sp = js_malloc_rt(rt, sizeof(JSAtomSymbol));
if (!sp)
/* Allocate simple JSString for symbol atoms (no payload needed) */
p = js_malloc_rt(rt, sizeof(JSString));
if (!p)
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
@@ -2651,14 +2640,6 @@ 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--;
@@ -2675,66 +2656,191 @@ 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)
/* Object-key map helpers for direct object key storage */
static inline uint32_t objkey_hash(JSObject *key_obj)
{
JSRuntime *rt = ctx->rt;
JSAtom atom = key_obj->object_key_atom;
return (uint32_t)((uintptr_t)key_obj >> 3);
}
/* 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])) {
/* Find slot for key_obj in objkey_tab. Returns index of key slot (even index).
If found is set to TRUE, the key exists at that slot.
If found is FALSE, the slot is either empty (JS_TAG_NULL) or a tombstone (JS_TAG_UNINITIALIZED)
and can be used for insertion. */
static uint32_t objkey_find_slot(JSValue *tab, uint32_t size, JSObject *key_obj, BOOL *found)
{
uint32_t mask = size - 1;
uint32_t h = objkey_hash(key_obj) & mask;
uint32_t i = h * 2; /* key at even index, value at odd */
uint32_t first_tombstone = UINT32_MAX;
JSAtomStruct *ap = rt->atom_array[atom];
if (ap->atom_type == JS_ATOM_TYPE_SYMBOL) {
JSAtomSymbol *sp = js_atom_as_symbol(ap);
for (;;) {
int tag = JS_VALUE_GET_TAG(tab[i]);
if (tag == JS_TAG_NULL) {
/* Empty slot */
*found = FALSE;
return (first_tombstone != UINT32_MAX) ? first_tombstone : i;
}
if (tag == JS_TAG_UNINITIALIZED) {
/* Tombstone - remember first one for insertion */
if (first_tombstone == UINT32_MAX)
first_tombstone = i;
} else if (JS_VALUE_GET_OBJ(tab[i]) == key_obj) {
/* Found */
*found = TRUE;
return i;
}
/* Linear probe */
h = (h + 1) & mask;
i = h * 2;
}
}
if (JS_VALUE_GET_TAG(sp->obj_key) == JS_TAG_OBJECT &&
JS_VALUE_GET_OBJ(sp->obj_key) == key_obj) {
return atom;
/* Get value for object key from this object only (no prototype chain) */
static JSValue js_get_objkey_own(JSContext *ctx, JSObject *p, JSObject *key_obj)
{
if (p->objkey_size == 0)
return JS_UNINITIALIZED; /* Not found marker */
BOOL found;
uint32_t slot = objkey_find_slot(p->objkey_tab, p->objkey_size, key_obj, &found);
if (found)
return JS_DupValue(ctx, p->objkey_tab[slot + 1]);
return JS_UNINITIALIZED;
}
/* Get value for object key, walking prototype chain */
static JSValue js_get_objkey_proto(JSContext *ctx, JSObject *p, JSObject *key_obj)
{
for (;;) {
JSValue val = js_get_objkey_own(ctx, p, key_obj);
if (!JS_IsUninitialized(val))
return val;
/* Walk prototype chain */
p = p->shape->proto;
if (!p)
return JS_NULL;
}
}
/* Resize objkey table */
static int objkey_resize(JSContext *ctx, JSObject *p, uint32_t new_size)
{
JSValue *new_tab = js_mallocz(ctx, new_size * 2 * sizeof(JSValue));
if (!new_tab)
return -1;
/* Initialize all slots to JS_NULL */
for (uint32_t i = 0; i < new_size * 2; i += 2) {
new_tab[i] = JS_NULL;
new_tab[i + 1] = JS_NULL;
}
/* Rehash existing entries */
if (p->objkey_tab) {
for (uint32_t i = 0; i < p->objkey_size * 2; i += 2) {
int tag = JS_VALUE_GET_TAG(p->objkey_tab[i]);
if (tag == JS_TAG_OBJECT) {
JSObject *key = JS_VALUE_GET_OBJ(p->objkey_tab[i]);
BOOL found;
uint32_t slot = objkey_find_slot(new_tab, new_size, key, &found);
new_tab[slot] = p->objkey_tab[i];
new_tab[slot + 1] = p->objkey_tab[i + 1];
}
}
js_free(ctx, p->objkey_tab);
}
p->objkey_tab = new_tab;
p->objkey_size = new_size;
return 0;
}
/* Set value for object key on this object. Returns -1 on error. */
static int js_set_objkey_own(JSContext *ctx, JSObject *p, JSObject *key_obj, JSValue val)
{
if (p->stone) {
JS_FreeValue(ctx, val);
JS_ThrowTypeError(ctx, "cannot modify frozen object");
return -1;
}
/* Allocate or resize if needed */
if (p->objkey_size == 0) {
if (objkey_resize(ctx, p, 4) < 0) {
JS_FreeValue(ctx, val);
return -1;
}
} else if (p->objkey_count * 2 >= p->objkey_size) {
/* Load factor > 0.5, resize */
if (objkey_resize(ctx, p, p->objkey_size * 2) < 0) {
JS_FreeValue(ctx, val);
return -1;
}
}
/* 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;
BOOL found;
uint32_t slot = objkey_find_slot(p->objkey_tab, p->objkey_size, key_obj, &found);
{
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));
if (found) {
/* Overwrite existing value */
JS_FreeValue(ctx, p->objkey_tab[slot + 1]);
p->objkey_tab[slot + 1] = val;
} else {
/* Insert new entry */
p->objkey_tab[slot] = JS_DupValue(ctx, JS_MKPTR(JS_TAG_OBJECT, key_obj));
p->objkey_tab[slot + 1] = val;
p->objkey_count++;
}
return 0;
}
/* Delete object key from this object. Returns TRUE if deleted, FALSE if not found, -1 on error. */
static int js_del_objkey_own(JSContext *ctx, JSObject *p, JSObject *key_obj)
{
if (p->objkey_size == 0)
return FALSE;
if (p->stone) {
JS_ThrowTypeError(ctx, "cannot modify frozen object");
return -1;
}
key_obj->object_key_atom = atom;
return atom;
BOOL found;
uint32_t slot = objkey_find_slot(p->objkey_tab, p->objkey_size, key_obj, &found);
if (!found)
return FALSE;
/* Free key and value, mark as tombstone */
JS_FreeValue(ctx, p->objkey_tab[slot]);
JS_FreeValue(ctx, p->objkey_tab[slot + 1]);
p->objkey_tab[slot] = JS_UNINITIALIZED;
p->objkey_tab[slot + 1] = JS_NULL;
p->objkey_count--;
return TRUE;
}
/* 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)
/* Check if object has object key (own property only) */
static BOOL js_has_objkey_own(JSObject *p, 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]);
if (p->objkey_size == 0)
return FALSE;
BOOL found;
objkey_find_slot(p->objkey_tab, p->objkey_size, key_obj, &found);
return found;
}
/* 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)
/* Check if object has object key (including prototype chain) */
static BOOL js_has_objkey_proto(JSObject *p, JSObject *key_obj)
{
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);
for (;;) {
if (js_has_objkey_own(p, key_obj))
return TRUE;
p = p->shape->proto;
if (!p)
return FALSE;
}
}
/* Warning: 'p' is freed */
@@ -4678,7 +4784,9 @@ static JSValue JS_NewObjectFromShape(JSContext *ctx, JSShape *sh, JSClassID clas
p->stone = FALSE;
p->free_mark = 0;
p->tmp_mark = 0;
p->object_key_atom = JS_ATOM_NULL;
p->objkey_tab = NULL;
p->objkey_count = 0;
p->objkey_size = 0;
p->u.opaque = NULL;
p->shape = sh;
p->prop = js_malloc(ctx, sizeof(JSProperty) * sh->prop_size);
@@ -5242,9 +5350,22 @@ static void free_object(JSRuntime *rt, JSObject *p)
putting it in gc_zero_ref_count_list */
js_free_shape(rt, sh);
/* Free object-key map entries */
if (p->objkey_tab) {
for (uint32_t j = 0; j < p->objkey_size * 2; j += 2) {
int tag = JS_VALUE_GET_TAG(p->objkey_tab[j]);
if (tag == JS_TAG_OBJECT) {
JS_FreeValueRT(rt, p->objkey_tab[j]);
JS_FreeValueRT(rt, p->objkey_tab[j + 1]);
}
}
js_free_rt(rt, p->objkey_tab);
}
/* fail safe */
p->shape = NULL;
p->prop = NULL;
p->objkey_tab = NULL;
finalizer = rt->class_array[p->class_id].finalizer;
if (finalizer)
@@ -5434,6 +5555,17 @@ static void mark_children(JSRuntime *rt, JSGCObjectHeader *gp,
if (gc_mark)
gc_mark(rt, JS_MKPTR(JS_TAG_OBJECT, p), mark_func);
}
/* Mark object-key map entries */
if (p->objkey_tab) {
for (uint32_t j = 0; j < p->objkey_size * 2; j += 2) {
int tag = JS_VALUE_GET_TAG(p->objkey_tab[j]);
if (tag == JS_TAG_OBJECT) {
JS_MarkValue(rt, p->objkey_tab[j], mark_func);
JS_MarkValue(rt, p->objkey_tab[j + 1], mark_func);
}
}
}
}
break;
case JS_GC_OBJ_TYPE_FUNCTION_BYTECODE:
@@ -6849,9 +6981,6 @@ 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) */
if (js_atom_is_object_key_symbol(ctx->rt, atom))
continue;
kind = JS_AtomGetKind(ctx, atom);
if ((kind == JS_ATOM_KIND_STRING && want_strings) ||
(kind == JS_ATOM_KIND_SYMBOL && want_symbols)) {
@@ -6877,9 +7006,6 @@ 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) */
if (js_atom_is_object_key_symbol(ctx->rt, atom))
continue;
kind = JS_AtomGetKind(ctx, atom);
if ((kind == JS_ATOM_KIND_STRING && want_strings) ||
(kind == JS_ATOM_KIND_SYMBOL && want_symbols)) {
@@ -6999,6 +7125,8 @@ JSAtom JS_ValueToAtom(JSContext *ctx, JSValueConst val)
if (tag == JS_TAG_INT) {
/* Convert integer to string atom */
atom = JS_NewAtomUInt32(ctx, (uint32_t)JS_VALUE_GET_INT(val));
} else if (tag == JS_TAG_OBJECT) {
return JS_ATOM_NULL;
} else if (tag == JS_TAG_SYMBOL) {
JSAtomStruct *p = JS_VALUE_GET_PTR(val);
atom = JS_DupAtom(ctx, js_get_atom_index(ctx->rt, p));
@@ -7008,7 +7136,6 @@ JSAtom JS_ValueToAtom(JSContext *ctx, JSValueConst val)
if (JS_IsException(str))
return JS_ATOM_NULL;
if (JS_VALUE_GET_TAG(str) == JS_TAG_SYMBOL) {
/* 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));
@@ -7058,7 +7185,7 @@ static JSValue JS_GetPropertyValue(JSContext *ctx, JSValueConst this_obj,
return JS_GetPropertyNumber(ctx, this_obj, idx);
}
if (prop_tag == JS_TAG_STRING || prop_tag == JS_TAG_STRING_ROPE || prop_tag == JS_TAG_OBJECT) {
if (prop_tag == JS_TAG_STRING || prop_tag == JS_TAG_STRING_ROPE) {
atom = JS_ValueToAtom(ctx, prop);
JS_FreeValue(ctx, prop);
ret = JS_GetProperty(ctx, this_obj, atom);
@@ -7066,6 +7193,21 @@ static JSValue JS_GetPropertyValue(JSContext *ctx, JSValueConst this_obj,
return ret;
}
/* Handle object keys directly via objkey map */
if (prop_tag == JS_TAG_OBJECT) {
if (this_tag != JS_TAG_OBJECT) {
JS_FreeValue(ctx, prop);
return JS_NULL;
}
JSObject *p = JS_VALUE_GET_OBJ(this_obj);
JSObject *key_obj = JS_VALUE_GET_OBJ(prop);
JS_FreeValue(ctx, prop);
JSValue val = js_get_objkey_proto(ctx, p, key_obj);
if (JS_IsUninitialized(val))
return JS_NULL;
return val;
}
/* Unknown type -> null */
JS_FreeValue(ctx, prop);
return JS_NULL;
@@ -7361,6 +7503,14 @@ static int JS_SetPropertyValue(JSContext *ctx, JSValueConst this_obj, JSValue pr
return -1;
}
/* Handle object keys directly via objkey map */
if (prop_tag == JS_TAG_OBJECT) {
JSObject *p = JS_VALUE_GET_OBJ(this_obj);
JSObject *key_obj = JS_VALUE_GET_OBJ(prop);
JS_FreeValue(ctx, prop);
return js_set_objkey_own(ctx, p, key_obj, val);
}
atom = JS_ValueToAtom(ctx, prop);
JS_FreeValue(ctx, prop);
if (unlikely(atom == JS_ATOM_NULL)) {
@@ -7411,9 +7561,21 @@ int JS_SetPropertyStr(JSContext *ctx, JSValueConst this_obj,
return ret;
}
/* Property access with JSValue key - supports object keys via symbols */
/* Property access with JSValue key - supports object keys directly */
JSValue JS_GetPropertyKey(JSContext *ctx, JSValueConst this_obj, JSValueConst key)
{
uint32_t tag = JS_VALUE_GET_TAG(key);
if (tag == JS_TAG_OBJECT) {
if (JS_VALUE_GET_TAG(this_obj) != JS_TAG_OBJECT)
return JS_NULL;
JSObject *p = JS_VALUE_GET_OBJ(this_obj);
JSObject *key_obj = JS_VALUE_GET_OBJ(key);
JSValue val = js_get_objkey_proto(ctx, p, key_obj);
if (JS_IsUninitialized(val))
return JS_NULL;
return val;
}
JSAtom atom;
JSValue ret;
atom = JS_ValueToAtom(ctx, key);
@@ -7426,6 +7588,18 @@ JSValue JS_GetPropertyKey(JSContext *ctx, JSValueConst this_obj, JSValueConst ke
int JS_SetPropertyKey(JSContext *ctx, JSValueConst this_obj, JSValueConst key, JSValue val)
{
uint32_t tag = JS_VALUE_GET_TAG(key);
if (tag == JS_TAG_OBJECT) {
if (JS_VALUE_GET_TAG(this_obj) != JS_TAG_OBJECT) {
JS_FreeValue(ctx, val);
JS_ThrowTypeError(ctx, "cannot set property on this value");
return -1;
}
JSObject *p = JS_VALUE_GET_OBJ(this_obj);
JSObject *key_obj = JS_VALUE_GET_OBJ(key);
return js_set_objkey_own(ctx, p, key_obj, val);
}
JSAtom atom;
int ret;
atom = JS_ValueToAtom(ctx, key);
@@ -7438,6 +7612,42 @@ int JS_SetPropertyKey(JSContext *ctx, JSValueConst this_obj, JSValueConst key, J
return ret;
}
/* Property existence check with JSValue key (supports object keys) */
int JS_HasPropertyKey(JSContext *ctx, JSValueConst obj, JSValueConst key)
{
uint32_t tag = JS_VALUE_GET_TAG(key);
if (tag == JS_TAG_OBJECT) {
if (JS_VALUE_GET_TAG(obj) != JS_TAG_OBJECT)
return FALSE;
return js_has_objkey_proto(JS_VALUE_GET_OBJ(obj), JS_VALUE_GET_OBJ(key));
}
JSAtom atom = JS_ValueToAtom(ctx, key);
if (atom == JS_ATOM_NULL)
return -1;
int ret = JS_HasProperty(ctx, obj, atom);
JS_FreeAtom(ctx, atom);
return ret;
}
/* Property deletion with JSValue key (supports object keys) */
int JS_DeletePropertyKey(JSContext *ctx, JSValueConst obj, JSValueConst key)
{
uint32_t tag = JS_VALUE_GET_TAG(key);
if (tag == JS_TAG_OBJECT) {
if (JS_VALUE_GET_TAG(obj) != JS_TAG_OBJECT)
return FALSE;
return js_del_objkey_own(ctx, JS_VALUE_GET_OBJ(obj), JS_VALUE_GET_OBJ(key));
}
JSAtom atom = JS_ValueToAtom(ctx, key);
if (atom == JS_ATOM_NULL)
return -1;
int ret = JS_DeleteProperty(ctx, obj, atom);
JS_FreeAtom(ctx, atom);
return ret;
}
/* compute the property flags. For each flag: (JS_PROP_HAS_x forces
it, otherwise def_flags is used)
Note: makes assumption about the bit pattern of the flags
@@ -8612,9 +8822,9 @@ JSValue JS_ToPropertyKey(JSContext *ctx, JSValueConst val)
{
int tag = JS_VALUE_GET_TAG(val);
/* Objects become their cached object-key symbol (identity-based key) */
/* Objects are handled directly via objkey map, not through atoms */
if (tag == JS_TAG_OBJECT) {
return js_get_object_key_symbol(ctx, JS_VALUE_GET_OBJ(val));
return JS_DupValue(ctx, val);
}
return JS_ToStringInternal(ctx, val, TRUE);
@@ -9709,7 +9919,6 @@ static no_inline int js_strict_eq_slow(JSContext *ctx, JSValue *sp,
static __exception int js_operator_in(JSContext *ctx, JSValue *sp)
{
JSValue op1, op2;
JSAtom atom;
int ret;
op1 = sp[-2];
@@ -9719,11 +9928,7 @@ static __exception int js_operator_in(JSContext *ctx, JSValue *sp)
JS_ThrowTypeError(ctx, "invalid 'in' operand");
return -1;
}
atom = JS_ValueToAtom(ctx, op1);
if (unlikely(atom == JS_ATOM_NULL))
return -1;
ret = JS_HasProperty(ctx, op2, atom);
JS_FreeAtom(ctx, atom);
ret = JS_HasPropertyKey(ctx, op2, op1);
if (ret < 0)
return -1;
JS_FreeValue(ctx, op1);
@@ -9735,16 +9940,12 @@ static __exception int js_operator_in(JSContext *ctx, JSValue *sp)
static __exception int js_operator_delete(JSContext *ctx, JSValue *sp)
{
JSValue op1, op2;
JSAtom atom;
int ret;
op1 = sp[-2];
op2 = sp[-1];
atom = JS_ValueToAtom(ctx, op2);
if (unlikely(atom == JS_ATOM_NULL))
return -1;
ret = JS_DeleteProperty(ctx, op1, atom);
JS_FreeAtom(ctx, atom);
ret = JS_DeletePropertyKey(ctx, op1, op2);
if (unlikely(ret < 0))
return -1;
JS_FreeValue(ctx, op1);