From 30e72eff79c0ff6985b3c11bcb122f2df7a67f12 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 5 Oct 2026 22:43:29 -0500 Subject: [PATCH] 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. --- .../test/ts7/baselines/order-parity.json | 6 + packages/runtime/src/scr_json.c | 185 ++++++++++++++---- packages/runtime/src/scr_json.test.c | 163 +++++++++++++++ packages/runtime/src/scr_json.test.ts | 21 +- packages/runtime/src/scr_runtime.h | 13 +- tests/corpus/checked-storage-transitions.js | 56 ++++++ 6 files changed, 390 insertions(+), 54 deletions(-) create mode 100644 tests/corpus/checked-storage-transitions.js diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 1ce05c5e7..aed38dcf4 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -14270,6 +14270,12 @@ "/tests/corpus/small-collection-transitions.ts" ], "diags": [] + }, + "/tests/corpus/checked-storage-transitions.js": { + "order": [ + "/tests/corpus/checked-storage-transitions.js" + ], + "diags": [] } } } diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index d50dda9f7..3d0b14d6d 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -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 = ¤t->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); diff --git a/packages/runtime/src/scr_json.test.c b/packages/runtime/src/scr_json.test.c index c52947fe7..a8e2c1539 100644 --- a/packages/runtime/src/scr_json.test.c +++ b/packages/runtime/src/scr_json.test.c @@ -1,8 +1,14 @@ #include "scr_runtime.h" #include +#include #include #include +#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++) { diff --git a/packages/runtime/src/scr_json.test.ts b/packages/runtime/src/scr_json.test.ts index 25fbd2acc..af140f0d9 100644 --- a/packages/runtime/src/scr_json.test.ts +++ b/packages/runtime/src/scr_json.test.ts @@ -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 }); } diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 8f4620a72..6918e1a67 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -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; diff --git a/tests/corpus/checked-storage-transitions.js b/tests/corpus/checked-storage-transitions.js new file mode 100644 index 000000000..b084719b3 --- /dev/null +++ b/tests/corpus/checked-storage-transitions.js @@ -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(","));