mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
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:
@@ -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
@@ -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);
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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(","));
|
||||
Reference in New Issue
Block a user