Reduce checked-value allocation and lookup overhead

- Reuse bounded property-key and small-integer storage.
- Cache validated property positions and avoid tracing primitive object members.
- Cover storage transitions, numeric edge cases, and shared-value lifetimes.
This commit is contained in:
Chris Tate
2026-10-05 22:43:29 -05:00
committed by GitHub
parent 0ceb78f886
commit 30e72eff79
6 changed files with 390 additions and 54 deletions
@@ -14270,6 +14270,12 @@
"<repo>/tests/corpus/small-collection-transitions.ts"
],
"diags": []
},
"<repo>/tests/corpus/checked-storage-transitions.js": {
"order": [
"<repo>/tests/corpus/checked-storage-transitions.js"
],
"diags": []
}
}
}
+142 -43
View File
@@ -413,6 +413,68 @@ void scr_dyn_from_leave(void) {
* strict alloc/free balance.
*/
static SCR_TL ScrDyn *scr_builtin_array_prototype;
/* Property names are immutable and often repeated across simultaneously live
* objects. Entries own references to their bytes; this fixed weak table owns
* none. Collisions only reduce sharing, and the final release removes a weak
* entry before freeing storage. Long keys bypass lookup entirely. */
#define SCR_DYN_KEY_SHARE_SLOTS 256
#define SCR_DYN_KEY_SHARE_LENGTH 64
typedef struct {
size_t rc;
char bytes[];
} ScrDynKey;
typedef struct {
ScrDynKey *key;
size_t length;
} ScrDynKeySlot;
static SCR_TL ScrDynKeySlot scr_dyn_keys[SCR_DYN_KEY_SHARE_SLOTS];
#ifdef SCR_RC_AUDIT
static SCR_TL long scr_live_dyn_keys;
long scr_dyn_key_live_count(void) { return scr_live_dyn_keys; }
#endif
static size_t scr_dyn_key_slot(const char *key, size_t length) {
size_t hash = length;
for (size_t i = 0; i < length; i++) hash = hash * 31 + (unsigned char)key[i];
return hash % SCR_DYN_KEY_SHARE_SLOTS;
}
static char *scr_dyn_key_copy(const char *key, size_t length) {
ScrDynKeySlot *slot = length <= SCR_DYN_KEY_SHARE_LENGTH
? &scr_dyn_keys[scr_dyn_key_slot(key, length)] : NULL;
if (slot && slot->key && slot->length == length &&
!memcmp(slot->key->bytes, key, length)) {
slot->key->rc++;
return slot->key->bytes;
}
if (length > SIZE_MAX - sizeof(ScrDynKey) - 1) scr_json_oom();
ScrDynKey *owned = malloc(sizeof *owned + length + 1);
if (!owned) scr_json_oom();
#ifdef SCR_RC_AUDIT
scr_live_dyn_keys++;
#endif
owned->rc = 1;
memcpy(owned->bytes, key, length);
owned->bytes[length] = '\0';
if (slot) *slot = (ScrDynKeySlot){owned, length};
return owned->bytes;
}
static void scr_dyn_key_release(char *key, size_t length) {
if (!key) return;
ScrDynKey *owned = (ScrDynKey *)(key - offsetof(ScrDynKey, bytes));
if (--owned->rc) return;
if (length <= SCR_DYN_KEY_SHARE_LENGTH) {
ScrDynKeySlot *slot = &scr_dyn_keys[scr_dyn_key_slot(key, length)];
if (slot->key == owned) slot->key = NULL;
}
#ifdef SCR_RC_AUDIT
scr_live_dyn_keys--;
#endif
free(owned);
}
#ifndef SCR_RC_AUDIT
static SCR_TL ScrDyn *scr_dyn_free_arr, *scr_dyn_free_obj, *scr_dyn_free_misc;
static SCR_TL size_t scr_dyn_free_count;
@@ -432,38 +494,51 @@ static void scr_dyn_obj_drop_keys(ScrDyn *d, bool cache) {
#ifndef SCR_RC_AUDIT
if (cache && d->v.obj.cap <= SCR_DYN_KEY_CACHE_SLOTS &&
entry->key_len <= SCR_DYN_KEY_CACHE_LENGTH &&
entry->key_len + 1 <= SCR_DYN_KEY_CACHE_MAX - scr_dyn_cached_key_bytes) {
scr_dyn_cached_key_bytes += entry->key_len + 1;
sizeof(ScrDynKey) + entry->key_len + 1 <= SCR_DYN_KEY_CACHE_MAX - scr_dyn_cached_key_bytes) {
scr_dyn_cached_key_bytes += sizeof(ScrDynKey) + entry->key_len + 1;
continue;
}
#else
(void)cache;
#endif
free(entry->key);
scr_dyn_key_release(entry->key, entry->key_len);
entry->key = NULL;
entry->key_len = 0;
}
}
/* Primitive kinds cannot acquire properties or change kind while live, so
* an object property edge to one cannot participate in a cycle. Keep its RC edge
* out of trial deletion and release it in collected teardown instead. Other
* owners may still trace the same primitive through the uniform header ABI;
* our untraced reference keeps it alive until that teardown releases it.
* Entry metadata snapshots this decision during trace, before any white node
* can be freed. Teardown must never inspect the child's kind again. */
static bool scr_dyn_member_is_traced(const ScrDyn *value) {
return value && value->kind > SCR_DYN_STR && value->kind != SCR_DYN_UNDEF;
}
/* Checked objects and functions can close over one another. All heap dyn
* nodes carry a collector header so a captured unknown value has one uniform
* tracing ABI, including when it currently contains a scalar. Opaque native
* capsules keep their conservative ownership edges. */
void scr_dyn_trace_v(void *ptr, ScrTraceVisit visit, void *ctx) {
ScrDyn *d = ptr;
visit(d->prototype, ctx);
visit(d->symbol_properties, ctx);
visit(d->symbol_keys, ctx);
if (d->prototype) visit(d->prototype, ctx);
if (d->symbol_properties) visit(d->symbol_properties, ctx);
if (d->symbol_keys) visit(d->symbol_keys, ctx);
switch (d->kind) {
case SCR_DYN_ARR:
for (size_t i = 0; i < d->v.arr.len; i++) visit(d->v.arr.items[i], ctx);
visit(d->v.arr.properties, ctx);
if (d->v.arr.properties) visit(d->v.arr.properties, ctx);
break;
case SCR_DYN_OBJ:
for (size_t i = 0; i < d->v.obj.len; i++) {
visit(d->v.obj.entries[i].value, ctx);
visit(d->v.obj.entries[i].getter, ctx);
visit(d->v.obj.entries[i].setter, ctx);
ScrDynEntry *entry = &d->v.obj.entries[i];
entry->value_traced = scr_dyn_member_is_traced(entry->value);
if (entry->value_traced) visit(entry->value, ctx);
if (entry->getter) visit(entry->getter, ctx);
if (entry->setter) visit(entry->setter, ctx);
}
break;
case SCR_DYN_FUNC: visit(d->v.fn.clo, ctx); break;
@@ -568,7 +643,7 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) {
if (d->v.obj.cap <= SCR_DYN_KEY_CACHE_SLOTS) {
for (size_t i = 0; i < d->v.obj.cap; i++) {
ScrDynEntry *entry = &d->v.obj.entries[i];
if (entry->key) scr_dyn_cached_key_bytes -= entry->key_len + 1;
if (entry->key) scr_dyn_cached_key_bytes -= sizeof(ScrDynKey) + entry->key_len + 1;
}
}
d->v.obj.source_identity = NULL;
@@ -624,8 +699,9 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) {
break;
case SCR_DYN_OBJ:
for (size_t i = 0; i < d->v.obj.len; i++) {
ScrDyn *value = d->v.obj.entries[i].value;
if (!collected || !d->v.obj.entries[i].value_traced) scr_dyn_release(value);
if (!collected) {
scr_dyn_release(d->v.obj.entries[i].value);
scr_dyn_release(d->v.obj.entries[i].getter);
scr_dyn_release(d->v.obj.entries[i].setter);
}
@@ -716,25 +792,43 @@ void scr_dyn_release(ScrDyn *d) {
scr_dyn_dispose(d, false);
}
/* A checked hint avoids repeating a linear walk for recurring property
* names. Slots store indices only, never object/key pointers. Every hit
* checks the current entry, so different shapes, deletion and reordering
* need no invalidation and collisions fall back to the ordinary search. */
#define SCR_DYN_PROPERTY_HINTS 64
static SCR_TL size_t scr_dyn_property_hints[SCR_DYN_PROPERTY_HINTS];
static ScrDynEntry *scr_dyn_find_entry(const ScrDyn *object, const char *key, size_t length) {
if (object->kind != SCR_DYN_OBJ) return NULL;
size_t slot = (length + (length ? (unsigned char)key[0] * 7u : 0)) % SCR_DYN_PROPERTY_HINTS;
size_t hint = scr_dyn_property_hints[slot];
if (hint < object->v.obj.len) {
ScrDynEntry *entry = &object->v.obj.entries[hint];
if (entry->key_len == length && !memcmp(entry->key, key, length)) return entry;
}
for (size_t i = 0; i < object->v.obj.len; i++) {
ScrDynEntry *entry = &object->v.obj.entries[i];
if (entry->key_len == length && !memcmp(entry->key, key, length)) {
scr_dyn_property_hints[slot] = i;
return entry;
}
}
return NULL;
}
ScrDyn *scr_dyn_obj_get(const ScrDyn *d, const char *key, size_t key_len) {
if (d->kind == SCR_DYN_PROXY) {
scr_dyn_proxy_unsupported("raw object inspection");
return NULL;
}
if (d->kind != SCR_DYN_OBJ) return NULL;
for (size_t i = 0; i < d->v.obj.len; i++) {
const ScrDynEntry *e = &d->v.obj.entries[i];
if (e->key_len == key_len && memcmp(e->key, key, key_len) == 0) return e->value;
}
return NULL;
ScrDynEntry *entry = scr_dyn_find_entry(d, key, key_len);
return entry ? entry->value : NULL;
}
bool scr_dyn_obj_enumerable(const ScrDyn *d, const char *key, size_t key_len) {
for (size_t i = 0; i < d->v.obj.len; i++) {
const ScrDynEntry *e = &d->v.obj.entries[i];
if (e->key_len == key_len && memcmp(e->key, key, key_len) == 0) return e->enumerable;
}
return false;
ScrDynEntry *entry = scr_dyn_find_entry(d, key, key_len);
return entry && entry->enumerable;
}
static const ScrDyn *scr_dyn_property_owner(const ScrDyn *object, const char *key, size_t length);
@@ -1056,11 +1150,9 @@ static void scr_dyn_obj_put(ScrDyn *obj, const char *key, size_t key_len, ScrDyn
}
ScrDynEntry *e = &obj->v.obj.entries[obj->v.obj.len++];
if (!e->key || e->key_len != key_len || memcmp(e->key, key, key_len)) {
free(e->key);
e->key = malloc(key_len + 1);
if (!e->key) scr_json_oom();
memcpy(e->key, key, key_len);
e->key[key_len] = '\0';
char *owned = scr_dyn_key_copy(key, key_len);
scr_dyn_key_release(e->key, e->key_len);
e->key = owned;
}
e->key_len = key_len;
e->value = value;
@@ -1130,6 +1222,22 @@ ScrDyn *scr_dyn_symbol_get(const ScrDyn *value, const ScrStr *key) {
}
ScrDyn *scr_dyn_new_num(double n) {
/* Small integers are immutable primitives, just like the boolean and null
* singletons. Initialize only touched slots; the table neither grows nor
* owns heap references. Preserve negative zero as a distinct payload. */
if (n >= -128 && n <= 255) {
int value = (int)n;
if ((double)value == n && (value != 0 || !signbit(n))) {
static SCR_TL ScrDyn values[384];
ScrDyn *cached = &values[value + 128];
if (cached->rc == 0) {
cached->rc = SIZE_MAX;
cached->kind = SCR_DYN_NUM;
cached->v.num = n;
}
return cached;
}
}
ScrDyn *d = scr_dyn_alloc(SCR_DYN_NUM);
d->v.num = n;
return d;
@@ -2880,12 +2988,7 @@ static ScrStr *scr_dyn_property_key(const ScrDyn *key) {
}
static ScrDynEntry *scr_dyn_entry(ScrDyn *obj, const ScrStr *key) {
if (obj->kind != SCR_DYN_OBJ) return NULL;
for (size_t i = 0; i < obj->v.obj.len; i++) {
ScrDynEntry *entry = &obj->v.obj.entries[i];
if (entry->key_len == key->len && memcmp(entry->key, key->data, key->len) == 0) return entry;
}
return NULL;
return scr_dyn_find_entry(obj, key->data, key->len);
}
static bool scr_dyn_property_same_value(const ScrDyn *a, const ScrDyn *b) {
@@ -5922,20 +6025,17 @@ static ScrDyn *scr_json_number(ScrJsonP *p) {
}
exp10 += eneg ? -ev : ev;
}
ScrDyn *d = scr_dyn_alloc(SCR_DYN_NUM);
if (precise && exp10 >= -22 && exp10 <= 22) {
double v = (double)mant; /* exact: mant < 10^15 < 2^53 */
if (exp10 > 0) v *= scr_json_pow10[exp10];
else if (exp10 < 0) v /= scr_json_pow10[-exp10];
d->v.num = neg ? -v : v;
return d;
return scr_dyn_new_num(neg ? -v : v);
}
/* The validated span re-parses with strtod (correctly rounded, and the
* grammar above is a strict subset of what strtod accepts). The ScrStr
* data is NUL-terminated, and strtod stops at the first non-number char,
* so parsing from `start` reads exactly the validated token. */
d->v.num = strtod(p->s + start, NULL);
return d;
return scr_dyn_new_num(strtod(p->s + start, NULL));
}
static bool scr_json_lit(ScrJsonP *p, const char *word, size_t n) {
@@ -6197,7 +6297,7 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) {
/* The moved tail aliases a live entry; spare slots own only their own
* retained key bytes, never a second pointer to a surviving property. */
memset(&object->v.obj.entries[object->v.obj.len], 0, sizeof *entry);
free(removed.key);
scr_dyn_key_release(removed.key, removed.key_len);
scr_dyn_release(removed.value);
scr_dyn_release(removed.getter);
scr_dyn_release(removed.setter);
@@ -6900,9 +7000,8 @@ static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_
continue;
}
if (current->kind != SCR_DYN_OBJ) continue;
for (size_t i = 0; i < current->v.obj.len; i++) {
ScrDynEntry *entry = &current->v.obj.entries[i];
if (entry->key_len != key_len || memcmp(entry->key, key, key_len) != 0) continue;
ScrDynEntry *entry = scr_dyn_find_entry(current, key, key_len);
if (entry) {
if (!entry->accessor) return scr_dyn_retain(entry->value);
if (!entry->getter) return scr_dyn_retain(scr_dyn_undefined());
ScrDyn *getter = scr_dyn_retain(entry->getter);
+163
View File
@@ -1,8 +1,14 @@
#include "scr_runtime.h"
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
#ifdef SCR_RC_AUDIT
long scr_str_live_count(void);
long scr_dyn_key_live_count(void);
#endif
static void put(ScrDyn *map, ScrDyn *key, ScrDyn *value) {
ScrDyn *args[] = {key, value};
ScrDyn *result = scr_dyn_handle_ops_of(map)->invoke(map->v.handle.ptr, map, "set", args, 2, "map.set");
@@ -91,9 +97,166 @@ static void checked_storage(void) {
assert(!scr_exc_pending());
}
static void shared_property_keys(void) {
#ifdef SCR_RC_AUDIT
long before = scr_dyn_key_live_count();
#endif
/* Distinct objects share bytes, not values or property attributes. Key
* deletion, a collected owner and a caller-owned input cannot invalidate
* the surviving entry. */
ScrDyn *first = scr_dyn_new_obj(), *second = scr_dyn_new_obj();
char key[] = "shared\0name";
scr_dyn_obj_set(first, key, sizeof key - 1, scr_dyn_new_num(1));
scr_dyn_obj_set(second, key, sizeof key - 1, scr_dyn_new_num(2));
assert(first->v.obj.entries[0].key == second->v.obj.entries[0].key);
key[0] = 'x';
ScrStr *name = scr_str_new("shared\0name", sizeof key - 1);
scr_dyn_key_delete(first, name, true);
scr_str_release(name);
assert(scr_dyn_obj_get(second, "shared\0name", sizeof key - 1)->v.num == 2);
scr_dyn_obj_set(first, "shared\0name", sizeof key - 1, scr_dyn_new_num(3));
scr_dyn_obj_set(first, "self", 4, scr_dyn_retain(first));
scr_dyn_release(first);
scr_collect_cycles();
assert(scr_dyn_obj_get(second, "shared\0name", sizeof key - 1)->v.num == 2);
scr_dyn_release(second);
/* More distinct live names than weak slots force collisions. Releasing
* an evicted owner must not clear or free the replacement's storage. */
ScrDyn *objects[600];
for (int round = 0; round < 2; round++) {
for (int i = 0; i < 600; i++) {
char bytes[32];
int length = snprintf(bytes, sizeof bytes, "field-%d", i);
objects[i] = scr_dyn_new_obj();
scr_dyn_obj_set(objects[i], bytes, (size_t)length, scr_dyn_new_num(i));
}
for (int i = 0; i < 600; i++) {
char bytes[32];
int length = snprintf(bytes, sizeof bytes, "field-%d", i);
assert(scr_dyn_obj_get(objects[i], bytes, (size_t)length)->v.num == i);
scr_dyn_release(objects[i]);
}
}
#ifdef SCR_RC_AUDIT
assert(scr_dyn_key_live_count() == before);
#endif
}
static void checked_leaf_cycles(void) {
/* Object properties omit primitives from trial deletion, while arrays and
* captured unknown values still trace them. Exercise shared leaves through
* both edge kinds, with and without an external owner, in either generation. */
for (int old = 0; old < 2; old++) {
for (int external = 0; external < 2; external++) {
#ifdef SCR_RC_AUDIT
long before_dyns = scr_dyn_live_count();
long before_strings = scr_str_live_count();
#endif
ScrStr *text = scr_str_new("survives either edge", 20);
ScrDyn *leaf = scr_dyn_new_str(text);
scr_str_release(text);
if (old) scr_cyc_hdr(leaf)->gen = SCR_CYC_MATURE;
ScrDyn *first = scr_dyn_new_obj(), *second = scr_dyn_new_obj();
ScrDyn *array = scr_dyn_new_arr();
ScrClosure *closure = scr_closure_new(NULL, 1);
closure->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
scr_box_set_ref(closure->caps[0], scr_dyn_retain(leaf));
scr_dyn_obj_set(first, "callback", 8, scr_dyn_new_func(closure, nothing, 0, "func()=>dyn", "leaf"));
scr_dyn_obj_set(first, "leaf", 4, scr_dyn_retain(leaf));
scr_dyn_obj_set(second, "leaf", 4, scr_dyn_retain(leaf));
scr_dyn_obj_set(second, "number", 6, scr_dyn_new_num(42));
scr_dyn_obj_set(second, "empty", 5, scr_dyn_new_null());
scr_dyn_obj_set(second, "missing", 7, scr_dyn_retain(scr_dyn_undefined()));
scr_dyn_arr_push(array, scr_dyn_retain(leaf));
scr_dyn_arr_push(array, scr_dyn_retain(first));
scr_dyn_obj_set(first, "next", 4, scr_dyn_retain(second));
scr_dyn_obj_set(second, "back", 4, scr_dyn_retain(first));
scr_dyn_obj_set(second, "array", 5, array);
if (!external) scr_dyn_release(leaf);
scr_dyn_release(first);
scr_dyn_release(second);
scr_cyc_collect_scheduled();
scr_collect_cycles();
if (external) {
assert(leaf->rc == 1 && leaf->v.str->len == 20);
scr_dyn_release(leaf);
}
#ifdef SCR_RC_AUDIT
assert(scr_dyn_live_count() == before_dyns);
assert(scr_str_live_count() == before_strings);
#endif
}
}
/* A previous trace snapshot must not survive a property mutation: the
* next collection must trace a new object edge or release a new leaf. */
for (int leaf_after = 0; leaf_after < 2; leaf_after++) {
#ifdef SCR_RC_AUDIT
long before = scr_dyn_live_count();
#endif
ScrDyn *object = scr_dyn_new_obj(), *child = scr_dyn_new_obj();
scr_dyn_obj_set(object, "self", 4, scr_dyn_retain(object));
scr_dyn_obj_set(object, "value", 5, leaf_after ? scr_dyn_retain(child) : scr_dyn_new_num(1));
scr_dyn_release(scr_dyn_retain(object));
scr_collect_cycles();
scr_dyn_obj_set(object, "value", 5, leaf_after ? scr_dyn_new_num(2) : scr_dyn_retain(child));
scr_dyn_obj_set(child, "parent", 6, scr_dyn_retain(object));
scr_dyn_release(child);
scr_dyn_release(object);
scr_collect_cycles();
#ifdef SCR_RC_AUDIT
assert(scr_dyn_live_count() == before);
#endif
}
}
static void checked_number_storage(void) {
#ifdef SCR_RC_AUDIT
long before = scr_dyn_live_count();
#endif
for (int value = -130; value < 258; value++) {
ScrDyn *first = scr_dyn_new_num(value), *second = scr_dyn_new_num(value);
assert(first->kind == SCR_DYN_NUM && first->v.num == value);
assert(scr_dyn_strict_eq(first, second));
if (value >= -128 && value <= 255) assert(first == second && first->rc == SIZE_MAX);
ScrDyn *array = scr_dyn_new_arr();
scr_dyn_arr_push(array, first);
scr_dyn_arr_push(array, second);
scr_dyn_arr_push(array, scr_dyn_retain(array));
scr_dyn_release(array);
scr_collect_cycles();
}
const double special[] = {-0.0, 0.5, -0.5, NAN, INFINITY, -INFINITY, 9007199254740991.0};
for (size_t i = 0; i < sizeof special / sizeof special[0]; i++) {
ScrDyn *value = scr_dyn_new_num(special[i]);
assert(value->rc != SIZE_MAX);
assert(isnan(special[i]) ? isnan(value->v.num) : value->v.num == special[i]);
if (special[i] == 0) assert(signbit(value->v.num));
scr_dyn_release(value);
}
const char raw[] = "[-0,0,1.0,1e2,-128,255,256,1e400]";
ScrStr *input = scr_str_new(raw, sizeof raw - 1);
ScrDyn *parsed = scr_json_parse(input);
assert(parsed && parsed->v.arr.len == 8);
assert(signbit(parsed->v.arr.items[0]->v.num));
assert(!signbit(parsed->v.arr.items[1]->v.num));
assert(parsed->v.arr.items[2] == scr_dyn_new_num(1));
assert(parsed->v.arr.items[3] == scr_dyn_new_num(100));
assert(isinf(parsed->v.arr.items[7]->v.num));
scr_dyn_release(parsed);
scr_str_release(input);
#ifdef SCR_RC_AUDIT
assert(scr_dyn_live_count() == before);
#endif
}
int main(void) {
scr_init();
checked_storage();
shared_property_keys();
checked_leaf_cycles();
checked_number_storage();
/* Native Set boxes may own headerless scalar/string maps. Collecting an
* enclosing cycle must neither trace those leaves nor skip their release. */
for (int i = 0; i < 2000; i++) {
+12 -9
View File
@@ -48,15 +48,18 @@ test.each([false, true])(
].map((file) => join(src, file)),
...(process.platform === "linux" ? ["-D_GNU_SOURCE", "-lm"] : []),
]);
const result = await exec(binary, [], {
env: {
...process.env,
ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1",
UBSAN_OPTIONS: "halt_on_error=1",
},
});
expect(result.stdout).toBe("checked storage and weak metadata lifetime checks passed\n");
expect(result.stderr).toBe("");
for (const threshold of ["1", "7", "256"]) {
const result = await exec(binary, [], {
env: {
...process.env,
SCR_CYCLE_THRESHOLD: threshold,
ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1",
UBSAN_OPTIONS: "halt_on_error=1",
},
});
expect(result.stdout).toBe("checked storage and weak metadata lifetime checks passed\n");
expect(result.stderr).toBe("");
}
} finally {
await rm(scratch, { recursive: true, force: true });
}
+11 -2
View File
@@ -3681,7 +3681,7 @@ typedef struct ScrJsval ScrJsval; /* opaque island cell (C11 repeat; the
* result as an owned (+1) dyn value. `args` entries are BORROWED. */
typedef ScrDyn *(*ScrDynThunk)(ScrClosure *clo, ScrDyn *const *args, size_t argc);
/* Object member. Keys are malloc'd UTF-8 bytes (NUL-terminated for
/* Object member. Keys own shared immutable UTF-8 bytes (NUL-terminated for
* convenience; key_len excludes the NUL) — duplicate keys were already
* collapsed at parse time (later wins, like JS JSON.parse). */
typedef struct {
@@ -3690,13 +3690,22 @@ typedef struct {
ScrDyn *value; /* owned */
ScrDyn *getter; /* owned when this is an accessor property */
ScrDyn *setter; /* owned when this is an accessor property */
bool accessor;
bool accessor : 1;
/* Snapshot written by trace before visiting children. Collected teardown
* must not inspect the value: an earlier cycle member may already be freed.
* Ordinary mutations need not maintain this collector-only decision. */
bool value_traced : 1;
/* Ordinary object insertion sets all three attributes true. */
bool writable;
bool enumerable;
bool configurable;
} ScrDynEntry;
_Static_assert(sizeof(ScrDynEntry) == (sizeof(void *) == 8 ? 48 : 24),
"LLVM checked-object readers use the target entry stride");
_Static_assert(offsetof(ScrDynEntry, value) == 2 * sizeof(void *),
"LLVM checked-object readers use the third pointer-sized field");
struct ScrDyn {
size_t rc; /* SIZE_MAX = immortal (unused for dyn; kept per convention) */
ScrDynKind kind;
@@ -0,0 +1,56 @@
// Repeated schemas, reordered fields and cycles share primitive values while
// preserving independent property storage, descriptors and insertion order.
function build(index) {
const left = JSON.parse('{"value":1,"other":2,"nul\\u0000key":3}');
const right = JSON.parse('{"other":4,"value":5,"nul\\u0000key":6}');
left.peer = right;
right.peer = left;
left.items = [right, left.value, "kept"];
right.items = [left, right.value, "kept"];
const saved = left.items.slice();
let total = left.value + right.value + left.other + right.other;
delete right.other;
right.other = index;
delete left.value;
left.value = right;
total += left.value.other;
left.value = -128;
Object.defineProperty(right, "value", {
get() { return left.value; }, configurable: true, enumerable: true
});
total += right.value + left["nul\0key"] + right["nul\0key"];
Object.defineProperty(right, "value", {
value: 255, writable: true, configurable: true, enumerable: true
});
if (saved[0] !== right || saved[1] !== 1 || saved[2] !== "kept") throw new Error("lost value");
if (index % 2 === 0) {
delete left.peer;
right.items.length = 0;
left.items = [];
}
return total + right.value;
}
let sum = 0;
for (let index = 0; index < 1200; index++) sum += build(index);
console.log(sum);
const numbers = JSON.parse("[-129,-128,-1,-0,0,1,1.0,1e2,255,256,0.5,1e400]");
console.log(numbers.map(value => Object.is(value, -0) ? "-0" : String(value)).join("|"));
console.log(new Set(numbers).size, numbers[5] === numbers[6]);
const kept = [];
for (let index = 0; index < 300; index++) {
const key = "field-" + index;
const value = JSON.parse('{"' + key + '":' + index + ',"shared":true}');
kept.push(value);
}
let keys = 0;
for (let index = 0; index < kept.length; index++) {
const value = kept[index];
keys += value["field-" + index];
delete value.shared;
value.shared = index;
keys += value.shared;
}
console.log(keys, Object.keys(kept[0]).join(","), Object.keys(kept[299]).join(","));