From 7ac0542aa98abd4a33a32b4679f95dbfa3bb431b Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Mon, 26 Oct 2026 05:39:34 -0700 Subject: [PATCH 1/2] Read ordinary objects' properties through accessors outside the object core Runtime units outside the object core (scr_json.c, scr_js_object.c) read and adjusted SCR_DYN_OBJ entries in place: v.obj.len, v.obj.entries[i] and the 48-byte ScrDynEntry fields. The single object model's generic objects (stage S3) change that storage, so these units now go through own-property accessors in scr_dyn.h: - scr_dyn_obj_count, scr_dyn_obj_key_at, scr_dyn_obj_value_at and scr_dyn_obj_prop_at (a ScrDynProp view: key, value, getter, setter and attributes) walk the properties in creation order; - scr_dyn_obj_index_of finds one by key; - scr_dyn_obj_set_attrs_at restricts a member a builtin object just added; - scr_dyn_own_symbol_prop and scr_dyn_copy_prop_at wrap the symbol-entry and copy primitives. The accessors are inline over today's entries, so behavior is unchanged. 21 units move (inspect, assert, util compare, console, http, http2, fetch, tls, fs, os, lib, array, message, publish, region, symbol, worker events, handle properties, builtin values, declaration adoption). A package test keeps entry storage reads inside the core. Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_array.c | 42 +++-- packages/runtime/src/scr_assert.c | 111 ++++++----- packages/runtime/src/scr_builtin_values.c | 3 +- packages/runtime/src/scr_bytes_io.c | 10 +- packages/runtime/src/scr_console_native.c | 30 ++- packages/runtime/src/scr_decl_adopt.c | 8 +- packages/runtime/src/scr_dyn.h | 75 ++++++++ packages/runtime/src/scr_dyn_handle_props.c | 27 +-- packages/runtime/src/scr_fetch.c | 58 +++--- packages/runtime/src/scr_http.c | 32 ++-- packages/runtime/src/scr_http2.c | 16 +- packages/runtime/src/scr_inspect.c | 197 +++++++++++--------- packages/runtime/src/scr_lib.c | 32 ++-- packages/runtime/src/scr_message.c | 21 ++- packages/runtime/src/scr_os.c | 19 +- packages/runtime/src/scr_publish.c | 8 +- packages/runtime/src/scr_region.c | 12 +- packages/runtime/src/scr_symbol.c | 15 +- packages/runtime/src/scr_tls.c | 94 +++++----- packages/runtime/src/scr_util_compare.c | 45 ++--- packages/runtime/src/scr_worker_events.c | 22 +-- packages/runtime/test/object-core.test.ts | 23 +++ 22 files changed, 508 insertions(+), 392 deletions(-) create mode 100644 packages/runtime/test/object-core.test.ts diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index b9e0efa74..c7132f528 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -5982,8 +5982,8 @@ void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) { target->v.arr.properties->v.obj.class_name = cls->name; ScrDyn *fields = scr_arr_class_fields(a); if (fields) { - for (size_t i = 0; i < fields->v.obj.len; i++) - scr_dyn_copy_entry(target, fields, &fields->v.obj.entries[i]); + for (size_t i = 0; i < scr_dyn_obj_count(fields); i++) + scr_dyn_copy_prop_at(target, fields, i); scr_dyn_release(fields); } return; @@ -5994,7 +5994,7 @@ void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) { return; metadata = SCR_AX(a)->metadata; if (!metadata) return; - for (size_t i = 0; i < metadata->v.obj.len; i++) scr_dyn_copy_entry(target, metadata, &metadata->v.obj.entries[i]); + for (size_t i = 0; i < scr_dyn_obj_count(metadata); i++) scr_dyn_copy_prop_at(target, metadata, i); } ScrArr *scr_arr_named_keys(ScrArr *a) { @@ -6008,9 +6008,9 @@ ScrArr *scr_arr_named_keys(ScrArr *a) { scr_arr_push_ref(keys, scr_str_new(x->props[i].key, x->props[i].key_len)); ScrDyn *fields = scr_arr_class_fields(a); if (fields) { - for (size_t i = 0; i < fields->v.obj.len; i++) { - const ScrDynEntry *entry = &fields->v.obj.entries[i]; - if (entry->enumerable) scr_arr_push_ref(keys, scr_str_new(entry->key, entry->key_len)); + for (size_t i = 0; i < scr_dyn_obj_count(fields); i++) { + ScrDynProp entry = scr_dyn_obj_prop_at(fields, i); + if (entry.enumerable) scr_arr_push_ref(keys, scr_str_new(entry.key, entry.key_len)); } scr_dyn_release(fields); return keys; @@ -6022,10 +6022,10 @@ ScrArr *scr_arr_named_keys(ScrArr *a) { return keys; } if (!metadata || metadata->kind != SCR_DYN_OBJ) return keys; - for (size_t i = 0; i < metadata->v.obj.len; i++) { - const ScrDynEntry *entry = &metadata->v.obj.entries[i]; - if (!entry->enumerable || scr_arr_prop_index(a, entry->key, entry->key_len) != SIZE_MAX) continue; - scr_arr_push_ref(keys, scr_str_new(entry->key, entry->key_len)); + for (size_t i = 0; i < scr_dyn_obj_count(metadata); i++) { + ScrDynProp entry = scr_dyn_obj_prop_at(metadata, i); + if (!entry.enumerable || scr_arr_prop_index(a, entry.key, entry.key_len) != SIZE_MAX) continue; + scr_arr_push_ref(keys, scr_str_new(entry.key, entry.key_len)); } return keys; } @@ -6038,9 +6038,9 @@ bool scr_arr_has_named_key(ScrArr *a, ScrStr *key) { if (metadata && metadata->kind == SCR_DYN_HANDLE && metadata->v.handle.tag == SCR_DYNH_MATCH_SOURCE) return true; /* index, input and groups, the only keys a pending result lists */ if (!metadata || metadata->kind != SCR_DYN_OBJ) return false; - for (size_t i = 0; i < metadata->v.obj.len; i++) { - const ScrDynEntry *entry = &metadata->v.obj.entries[i]; - if (entry->key_len == key->len && memcmp(entry->key, key->data, key->len) == 0) return true; + for (size_t i = 0; i < scr_dyn_obj_count(metadata); i++) { + ScrDynProp entry = scr_dyn_obj_prop_at(metadata, i); + if (entry.key_len == key->len && memcmp(entry.key, key->data, key->len) == 0) return true; } return false; } @@ -6067,20 +6067,22 @@ void scr_arr_adopt_view_properties(ScrArr *a, ScrDyn *view, ScrArr *target) { from->prop_cap = cap; } ScrDyn *named = view && view->kind == SCR_DYN_ARR ? view->v.arr.properties : NULL; - if (!named || named->v.obj.len == 0) return; + if (!named || scr_dyn_obj_count(named) == 0) return; /* The view's numeric properties are copies of the native ones (every * write to them goes to the native array), so only the others move. */ bool numeric = false; - for (size_t i = 0; i < named->v.obj.len && !numeric; i++) { + for (size_t i = 0; i < scr_dyn_obj_count(named) && !numeric; i++) { double number; - numeric = scr_arr_numeric_key(named->v.obj.entries[i].key, named->v.obj.entries[i].key_len, &number); + size_t key_len; + const char *key = scr_dyn_obj_key_at(named, i, &key_len); + numeric = scr_arr_numeric_key(key, key_len, &number); } if (numeric) { ScrDyn *rest = scr_dyn_new_obj(); - for (size_t i = 0; i < named->v.obj.len; i++) { - const ScrDynEntry *entry = &named->v.obj.entries[i]; + for (size_t i = 0; i < scr_dyn_obj_count(named); i++) { + ScrDynProp entry = scr_dyn_obj_prop_at(named, i); double number; - if (!scr_arr_numeric_key(entry->key, entry->key_len, &number)) scr_dyn_copy_entry(rest, named, entry); + if (!scr_arr_numeric_key(entry.key, entry.key_len, &number)) scr_dyn_copy_prop_at(rest, named, i); } named = rest; } else { @@ -6088,7 +6090,7 @@ void scr_arr_adopt_view_properties(ScrArr *a, ScrDyn *view, ScrArr *target) { } ScrArrExt *x = scr_arr_ext(a); ScrDyn *old = x->metadata; - x->metadata = named->v.obj.len ? named : NULL; + x->metadata = scr_dyn_obj_count(named) ? named : NULL; if (!x->metadata) scr_dyn_release(named); scr_dyn_release(old); } diff --git a/packages/runtime/src/scr_assert.c b/packages/runtime/src/scr_assert.c index bf5d1c875..6e9384f15 100644 --- a/packages/runtime/src/scr_assert.c +++ b/packages/runtime/src/scr_assert.c @@ -718,7 +718,7 @@ static bool scr_assert_dyn_deep_eq(const ScrDyn *a, const ScrDyn *b) { if (a->v.arr.properties || b->v.arr.properties) { if (!a->v.arr.properties || !b->v.arr.properties) { const ScrDyn *table = a->v.arr.properties ? a->v.arr.properties : b->v.arr.properties; - if (table->v.obj.len != 0) return false; + if (scr_dyn_obj_count(table) != 0) return false; } else if (!scr_assert_dyn_deep_eq(a->v.arr.properties, b->v.arr.properties)) return false; } return true; @@ -738,11 +738,11 @@ static bool scr_assert_dyn_deep_eq(const ScrDyn *a, const ScrDyn *b) { } else if (a->null_proto != b->null_proto || a->prototype != b->prototype) { return false; } - if (a->v.obj.len != b->v.obj.len) return false; - for (size_t i = 0; i < a->v.obj.len; i++) { - const ScrDynEntry *ent = &a->v.obj.entries[i]; - ScrDyn *bv = scr_dyn_obj_get(b, ent->key, ent->key_len); - if (!bv || !scr_assert_dyn_deep_eq(ent->value, bv)) return false; + if (scr_dyn_obj_count(a) != scr_dyn_obj_count(b)) return false; + for (size_t i = 0; i < scr_dyn_obj_count(a); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(a, i); + ScrDyn *bv = scr_dyn_obj_get(b, ent.key, ent.key_len); + if (!bv || !scr_assert_dyn_deep_eq(ent.value, bv)) return false; } return true; } @@ -782,30 +782,29 @@ static bool scr_assert_loose_nan(const ScrDyn *d) { return d->kind == SCR_DYN_NUM && d->v.num != d->v.num; } -/* An enumerable own entry, or NULL. Borrowed. */ -static const ScrDynEntry *scr_assert_loose_entry(const ScrDyn *d, const char *key, size_t len) { - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *ent = &d->v.obj.entries[i]; - if (ent->key_len == len && memcmp(ent->key, key, len) == 0) return ent->enumerable ? ent : NULL; - } - return NULL; +/* The value of an enumerable own property, or NULL. Borrowed. */ +static const ScrDyn *scr_assert_loose_entry(const ScrDyn *d, const char *key, size_t len) { + size_t i = scr_dyn_obj_index_of(d, key, len); + if (i == SIZE_MAX) return NULL; + ScrDynProp ent = scr_dyn_obj_prop_at(d, i); + return ent.enumerable ? ent.value : NULL; } -static bool scr_assert_loose_counted(const ScrDynEntry *ent) { - return ent->enumerable && !(ent->key_len == 6 && memcmp(ent->key, "%error", 6) == 0); +static bool scr_assert_loose_counted(ScrDynProp ent) { + return ent.enumerable && !(ent.key_len == 6 && memcmp(ent.key, "%error", 6) == 0); } /* keyCheck and objEquiv over two objects' enumerable own keys. */ static bool scr_assert_loose_keys(const ScrDyn *a, const ScrDyn *b) { size_t na = 0, nb = 0; - for (size_t i = 0; i < a->v.obj.len; i++) na += scr_assert_loose_counted(&a->v.obj.entries[i]); - for (size_t i = 0; i < b->v.obj.len; i++) nb += scr_assert_loose_counted(&b->v.obj.entries[i]); + for (size_t i = 0; i < scr_dyn_obj_count(a); i++) na += scr_assert_loose_counted(scr_dyn_obj_prop_at(a, i)); + for (size_t i = 0; i < scr_dyn_obj_count(b); i++) nb += scr_assert_loose_counted(scr_dyn_obj_prop_at(b, i)); if (na != nb) return false; - for (size_t i = 0; i < b->v.obj.len; i++) { - const ScrDynEntry *ent = &b->v.obj.entries[i]; + for (size_t i = 0; i < scr_dyn_obj_count(b); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(b, i); if (!scr_assert_loose_counted(ent)) continue; - const ScrDynEntry *other = scr_assert_loose_entry(a, ent->key, ent->key_len); - if (!other || !scr_assert_loose_eq(other->value, ent->value)) return false; + const ScrDyn *other = scr_assert_loose_entry(a, ent.key, ent.key_len); + if (!other || !scr_assert_loose_eq(other, ent.value)) return false; } return true; } @@ -1180,8 +1179,8 @@ static bool scr_assert_loose_array(const ScrDyn *a, const ScrDyn *b) { if (pa || pb) { if (!pa || !pb) { const ScrDyn *table = pa ? pa : pb; - for (size_t i = 0; i < table->v.obj.len; i++) - if (scr_assert_loose_counted(&table->v.obj.entries[i])) return false; + for (size_t i = 0; i < scr_dyn_obj_count(table); i++) + if (scr_assert_loose_counted(scr_dyn_obj_prop_at(table, i))) return false; } else if (!scr_assert_loose_keys(pa, pb)) { return false; } @@ -1295,7 +1294,7 @@ static void scr_assert_cf_value(ScrAssertBuf *b, const ScrDyn *d, size_t indent, * or `{`, then its entries. */ static void scr_assert_cf_object(ScrAssertBuf *b, const ScrDyn *d, size_t indent, size_t depth, const char *cls) { - if (d->v.obj.len == 0) { + if (scr_dyn_obj_count(d) == 0) { if (cls) { ab_cstr(b, cls); ab_cstr(b, " {}"); @@ -1320,25 +1319,25 @@ static void scr_assert_cf_object(ScrAssertBuf *b, const ScrDyn *d, size_t indent } else if (d->null_proto) ab_cstr(b, "[Object: null prototype] "); /* Render every entry, then sort the RENDERED texts (Node's * sorted:true sorts formatted entries, quotes included). */ - ScrCfEntry *ents = malloc(d->v.obj.len * sizeof *ents); + ScrCfEntry *ents = malloc(scr_dyn_obj_count(d) * sizeof *ents); if (!ents) scr_assert_oom(); - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *ent = &d->v.obj.entries[i]; + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(d, i); ScrAssertBuf eb = {0}; - scr_assert_cf_key(&eb, ent->key, ent->key_len); + scr_assert_cf_key(&eb, ent.key, ent.key_len); ab_cstr(&eb, ": "); - scr_assert_cf_value(&eb, ent->value, indent + 2, depth - 1); + scr_assert_cf_value(&eb, ent.value, indent + 2, depth - 1); ents[i].text = eb.data ? eb.data : malloc(1); if (!ents[i].text) scr_assert_oom(); ents[i].len = eb.len; } - qsort(ents, d->v.obj.len, sizeof *ents, scr_assert_cf_entry_cmp); + qsort(ents, scr_dyn_obj_count(d), sizeof *ents, scr_assert_cf_entry_cmp); ab_char(b, '{'); - for (size_t i = 0; i < d->v.obj.len; i++) { + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { ab_char(b, '\n'); scr_assert_cf_pad(b, indent + 2); ab_bytes(b, ents[i].text, ents[i].len); - if (i + 1 < d->v.obj.len) ab_char(b, ','); + if (i + 1 < scr_dyn_obj_count(d)) ab_char(b, ','); free(ents[i].text); } free(ents); @@ -1364,11 +1363,11 @@ static const char *scr_assert_cf_bytes_class(ScrBytesElem elem) { } -static bool scr_assert_cf_error_extra(const ScrDynEntry *ent) { - if (!ent->enumerable || (ent->key_len > 0 && ent->key[0] == '%')) return false; - return !((ent->key_len == 4 && memcmp(ent->key, "name", 4) == 0) || - (ent->key_len == 7 && memcmp(ent->key, "message", 7) == 0) || - (ent->key_len == 5 && memcmp(ent->key, "stack", 5) == 0)); +static bool scr_assert_cf_error_extra(ScrDynProp ent) { + if (!ent.enumerable || (ent.key_len > 0 && ent.key[0] == '%')) return false; + return !((ent.key_len == 4 && memcmp(ent.key, "name", 4) == 0) || + (ent.key_len == 7 && memcmp(ent.key, "message", 7) == 0) || + (ent.key_len == 5 && memcmp(ent.key, "stack", 5) == 0)); } /* An encoded Error: Node's stackless `[Name: message]` (assertion_error.js @@ -1386,8 +1385,8 @@ static void scr_assert_cf_error(ScrAssertBuf *b, const ScrDyn *d, size_t indent, } ab_char(b, ']'); size_t extra = 0; - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *ent = &d->v.obj.entries[i]; + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(d, i); if (scr_assert_cf_error_extra(ent)) extra++; } if (extra == 0) return; @@ -1395,13 +1394,13 @@ static void scr_assert_cf_error(ScrAssertBuf *b, const ScrDyn *d, size_t indent, ScrCfEntry *ents = malloc(extra * sizeof *ents); if (!ents) scr_assert_oom(); size_t n = 0; - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *ent = &d->v.obj.entries[i]; + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(d, i); if (!scr_assert_cf_error_extra(ent)) continue; ScrAssertBuf eb = {0}; - scr_assert_cf_key(&eb, ent->key, ent->key_len); + scr_assert_cf_key(&eb, ent.key, ent.key_len); ab_cstr(&eb, ": "); - scr_assert_cf_value(&eb, ent->value, indent + 2, depth - 1); + scr_assert_cf_value(&eb, ent.value, indent + 2, depth - 1); ents[n].text = eb.data ? eb.data : malloc(1); if (!ents[n].text) scr_assert_oom(); ents[n++].len = eb.len; @@ -1989,11 +1988,11 @@ static bool scr_assert_partial_eq(const ScrDyn *a, const ScrDyn *b); /* objEquiv over the expected side's enumerable own keys. */ static bool scr_assert_partial_keys(const ScrDyn *a, const ScrDyn *b) { - for (size_t i = 0; i < b->v.obj.len; i++) { - const ScrDynEntry *ent = &b->v.obj.entries[i]; + for (size_t i = 0; i < scr_dyn_obj_count(b); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(b, i); if (!scr_assert_loose_counted(ent)) continue; - const ScrDynEntry *other = scr_assert_loose_entry(a, ent->key, ent->key_len); - if (!other || !scr_assert_partial_eq(other->value, ent->value)) return false; + const ScrDyn *other = scr_assert_loose_entry(a, ent.key, ent.key_len); + if (!other || !scr_assert_partial_eq(other, ent.value)) return false; } return true; } @@ -2043,11 +2042,11 @@ static bool scr_assert_partial_array(const ScrDyn *a, const ScrDyn *b) { const ScrDyn *pb = b->v.arr.properties; if (pb) { const ScrDyn *pa = a->v.arr.properties; - for (size_t i = 0; i < pb->v.obj.len; i++) { - const ScrDynEntry *ent = &pb->v.obj.entries[i]; + for (size_t i = 0; i < scr_dyn_obj_count(pb); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(pb, i); if (!scr_assert_loose_counted(ent)) continue; - const ScrDynEntry *other = pa ? scr_assert_loose_entry(pa, ent->key, ent->key_len) : NULL; - if (!other || !scr_assert_partial_eq(other->value, ent->value)) return false; + const ScrDyn *other = pa ? scr_assert_loose_entry(pa, ent.key, ent.key_len) : NULL; + if (!other || !scr_assert_partial_eq(other, ent.value)) return false; } } size_t pos = 0; @@ -2434,11 +2433,11 @@ void scr_assert_loose_deep_result(bool equal, bool negated, ScrStr *msg, bool ha void scr_assert_expects_err_dyn(ScrDyn *actual, ScrDyn *expected, ScrStr *msg, bool has_msg) { if (actual->kind == SCR_DYN_OBJ && expected->kind == SCR_DYN_OBJ) { bool ok = true; - for (size_t i = 0; i < expected->v.obj.len && ok; i++) { - const ScrDynEntry *e = &expected->v.obj.entries[i]; - if (e->key_len == 6 && memcmp(e->key, "%error", 6) == 0) continue; - ScrDyn *av = scr_dyn_obj_get(actual, e->key, e->key_len); - if (av == NULL || !scr_assert_dyn_deep_eq(av, e->value)) ok = false; + for (size_t i = 0; i < scr_dyn_obj_count(expected) && ok; i++) { + ScrDynProp e = scr_dyn_obj_prop_at(expected, i); + if (e.key_len == 6 && memcmp(e.key, "%error", 6) == 0) continue; + ScrDyn *av = scr_dyn_obj_get(actual, e.key, e.key_len); + if (av == NULL || !scr_assert_dyn_deep_eq(av, e.value)) ok = false; } if (ok) return; } diff --git a/packages/runtime/src/scr_builtin_values.c b/packages/runtime/src/scr_builtin_values.c index f8dd2b79b..084a1d50d 100644 --- a/packages/runtime/src/scr_builtin_values.c +++ b/packages/runtime/src/scr_builtin_values.c @@ -441,8 +441,7 @@ ScrDyn *scr_buffer_constructor_value(void) { ScrDyn *table = scr_dyn_function_properties(scr_bv_buffer); ScrDyn *getter = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_bv_prototype, 0, "native:function", "get prototype"); scr_dyn_obj_set_accessor(table, "prototype", 9, getter, NULL); - ScrDynEntry *entry = &table->v.obj.entries[table->v.obj.len - 1]; - entry->enumerable = entry->configurable = false; + scr_dyn_obj_set_attrs_at(table, scr_dyn_obj_count(table) - 1, false, false, false); scr_dyn_obj_set(table, "poolSize", 8, scr_dyn_new_num(pool_size)); ScrDyn *is_encoding = NULL; for (size_t i = 0; i < sizeof statics / sizeof statics[0]; i++) { diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index daeb47c60..0c4bee4ee 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1757,11 +1757,11 @@ ScrStr *scr_fs_mkdtemp_sync_chk(const ScrDyn *prefix, const ScrDyn *opts, const if (!scr_fs_encoding_chk(opts)) return NULL; bool utf8 = true; if (opts->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < opts->v.obj.len; i++) { - const ScrDynEntry *e = &opts->v.obj.entries[i]; - if (scr_fs_dyn_absent(e->value)) continue; - if (strcmp(e->key, "encoding") == 0) { - if (!scr_fs_utf8_encoding(e->value)) utf8 = false; + for (size_t i = 0; i < scr_dyn_obj_count(opts); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(opts, i); + if (scr_fs_dyn_absent(e.value)) continue; + if (strcmp(e.key, "encoding") == 0) { + if (!scr_fs_utf8_encoding(e.value)) utf8 = false; continue; } utf8 = false; /* an unmodeled effectful option */ diff --git a/packages/runtime/src/scr_console_native.c b/packages/runtime/src/scr_console_native.c index 492d57d82..e11ffd3af 100644 --- a/packages/runtime/src/scr_console_native.c +++ b/packages/runtime/src/scr_console_native.c @@ -507,8 +507,8 @@ static bool scr_console_holds_buffer(const ScrDyn *d) { if (scr_console_holds_buffer(d->v.arr.items[i])) return true; return false; case SCR_DYN_OBJ: - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDyn *value = d->v.obj.entries[i].value; + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + const ScrDyn *value = scr_dyn_obj_value_at(d, i); if (value && scr_console_holds_buffer(value)) return true; } return false; @@ -626,7 +626,7 @@ static ScrStr **scr_table_own_keys(const ScrDyn *item, size_t *count) { return keys; } if (item->kind != SCR_DYN_OBJ) return NULL; - size_t len = item->v.obj.len; + size_t len = scr_dyn_obj_count(item); const char **names = calloc(len ? len : 1, sizeof *names); size_t *lens = calloc(len ? len : 1, sizeof *lens); size_t *order = calloc(len ? len : 1, sizeof *order); @@ -634,10 +634,10 @@ static ScrStr **scr_table_own_keys(const ScrDyn *item, size_t *count) { if (!names || !lens || !order || !keys) scr_trap("scriptc: out of memory\n"); size_t n = 0; for (size_t i = 0; i < len; i++) { - const ScrDynEntry *ent = &item->v.obj.entries[i]; - if (!ent->enumerable) continue; - names[n] = ent->key; - lens[n] = ent->key_len; + ScrDynProp prop = scr_dyn_obj_prop_at(item, i); + if (!prop.enumerable) continue; + names[n] = prop.key; + lens[n] = prop.key_len; n++; } scr_table_key_order(names, lens, n, order); @@ -652,16 +652,14 @@ static ScrStr **scr_table_own_keys(const ScrDyn *item, size_t *count) { /* item[key] when it is an own property; NULL otherwise. +1. */ static ScrDyn *scr_table_own_value(const ScrDyn *item, const ScrStr *key) { if (item->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < item->v.obj.len; i++) { - const ScrDynEntry *ent = &item->v.obj.entries[i]; - if (ent->key_len != key->len || memcmp(ent->key, key->data, key->len) != 0) continue; - if (ent->accessor) { - scr_table_fence("console.table rows with accessor properties have no lowering"); - return NULL; - } - return scr_dyn_retain(ent->value); + size_t i = scr_dyn_obj_index_of(item, key->data, key->len); + if (i == SIZE_MAX) return NULL; + ScrDynProp prop = scr_dyn_obj_prop_at(item, i); + if (prop.accessor) { + scr_table_fence("console.table rows with accessor properties have no lowering"); + return NULL; } - return NULL; + return scr_dyn_retain(prop.value); } if (item->kind == SCR_DYN_ARR || item->kind == SCR_DYN_BYTES) { size_t len = item->kind == SCR_DYN_ARR ? item->v.arr.len : item->v.bytes->len; diff --git a/packages/runtime/src/scr_decl_adopt.c b/packages/runtime/src/scr_decl_adopt.c index fc1c4456c..ffac1f802 100644 --- a/packages/runtime/src/scr_decl_adopt.c +++ b/packages/runtime/src/scr_decl_adopt.c @@ -63,10 +63,10 @@ static bool scr_decl_adopt_plain(const ScrDyn *n, const ScrDynPath *path, bool l else if (n->v.obj.source_access && !n->v.obj.source_identity) want = "a plain object"; else { - for (size_t i = 0; i < n->v.obj.len && !want; i++) { - const ScrDynEntry *e = &n->v.obj.entries[i]; - if (e->accessor) want = "a plain object (this value has accessor properties)"; - else if (!e->writable || !e->enumerable || !e->configurable) + for (size_t i = 0; i < scr_dyn_obj_count(n) && !want; i++) { + ScrDynProp e = scr_dyn_obj_prop_at(n, i); + if (e.accessor) want = "a plain object (this value has accessor properties)"; + else if (!e.writable || !e.enumerable || !e.configurable) want = "a plain object (this value has non-default property attributes)"; } } diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index a71aaa244..ebb274a23 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -584,6 +584,67 @@ _Static_assert(sizeof(((ScrDyn *)0)->v.arr) <= sizeof(((ScrDyn *)0)->v.typed_ref _Static_assert(sizeof(void *) != 8 || offsetof(ScrDyn, v.obj.shape) == 80, "v.obj.shape offset"); _Static_assert(sizeof(void *) != 8 || offsetof(ScrDyn, v.obj.region) == 72, "v.obj.region offset"); +/* ── own properties of ordinary objects ────────────────────────────── + * Code outside the object core (scr_json.c and scr_js_object.c) reads and + * adjusts an SCR_DYN_OBJ's own string-keyed properties through these + * accessors, never through v.obj.entries, so the object's storage can + * change underneath (the single object model's generic objects). Index i + * runs over the properties in creation order, 0 <= i < scr_dyn_obj_count. + * Keys and values are borrowed: they stay valid until the object changes. */ +typedef struct { + const char *key; + size_t key_len; + ScrDyn *value; /* data property; the undefined value for an accessor */ + ScrDyn *getter; /* accessor property; NULL when absent */ + ScrDyn *setter; /* accessor property; NULL when absent */ + bool accessor; + bool writable; + bool enumerable; + bool configurable; +} ScrDynProp; + +static inline size_t scr_dyn_obj_count(const ScrDyn *o) { + return o->v.obj.len; +} + +static inline const char *scr_dyn_obj_key_at(const ScrDyn *o, size_t i, size_t *key_len) { + *key_len = o->v.obj.entries[i].key_len; + return o->v.obj.entries[i].key; +} + +/* The value of data property i (borrowed); the undefined value for an + * accessor property, whose functions scr_dyn_obj_prop_at answers. */ +static inline ScrDyn *scr_dyn_obj_value_at(const ScrDyn *o, size_t i) { + return o->v.obj.entries[i].value; +} + +static inline ScrDynProp scr_dyn_obj_prop_at(const ScrDyn *o, size_t i) { + const ScrDynEntry *e = &o->v.obj.entries[i]; + return (ScrDynProp){e->key, e->key_len, e->value, e->getter, e->setter, + e->accessor, e->writable, e->enumerable, e->configurable}; +} + +/* The index of own property `key`, or SIZE_MAX. A linear scan: for code + * that looks up options and headers, not for hot property reads. */ +static inline size_t scr_dyn_obj_index_of(const ScrDyn *o, const char *key, size_t key_len) { + for (size_t i = 0; i < o->v.obj.len; i++) { + const ScrDynEntry *e = &o->v.obj.entries[i]; + if (e->key_len == key_len && memcmp(e->key, key, key_len) == 0) return i; + } + return SIZE_MAX; +} + +/* Sets the attributes of property i (`writable` is ignored for an + * accessor property): builtin objects the runtime builds restrict their + * members this way right after adding them. */ +static inline void scr_dyn_obj_set_attrs_at(ScrDyn *o, size_t i, bool writable, bool enumerable, + bool configurable) { + ScrDynEntry *e = &o->v.obj.entries[i]; + if (!e->accessor) e->writable = writable; + e->enumerable = enumerable; + e->configurable = configurable; +} + /* ── intact parse regions ───────────────────────────────────────────── * Every heap dyn node carries a cycle header, so an ordinary release that * leaves an array or object alive buffers it as a possible cycle root, and @@ -748,6 +809,20 @@ ScrDyn *scr_dyn_get_own_property_symbols(const ScrDyn *value); /* The own property stored under a symbol key on an object (borrowed), or * NULL when it has none. */ ScrDynEntry *scr_dyn_own_symbol_entry(const ScrDyn *object, const ScrDyn *key); +/* The own symbol-keyed property `key` of `object` as a property view + * (scr_dyn_obj_prop_at; `key` and `key_len` name its private storage + * key); false when there is none. */ +static inline bool scr_dyn_own_symbol_prop(const ScrDyn *object, const ScrDyn *key, ScrDynProp *out) { + const ScrDynEntry *e = scr_dyn_own_symbol_entry(object, key); + if (!e) return false; + *out = (ScrDynProp){e->key, e->key_len, e->value, e->getter, e->setter, + e->accessor, e->writable, e->enumerable, e->configurable}; + return true; +} +/* Copies own property i of `source` to `target` (scr_dyn_copy_entry). */ +static inline void scr_dyn_copy_prop_at(ScrDyn *target, const ScrDyn *source, size_t i) { + scr_dyn_copy_entry(target, source, &source->v.obj.entries[i]); +} /* The private storage name of a symbol key (+1). With it, * scr_dyn_symbol_init_own gives an object that has no symbol properties * yet its first one, a writable, enumerable, configurable data property diff --git a/packages/runtime/src/scr_dyn_handle_props.c b/packages/runtime/src/scr_dyn_handle_props.c index c7de963f0..88677836f 100644 --- a/packages/runtime/src/scr_dyn_handle_props.c +++ b/packages/runtime/src/scr_dyn_handle_props.c @@ -60,17 +60,17 @@ static ScrDyn *hp_props_of(void *child) { return props; } -static ScrDynEntry *hp_entry(const ScrDyn *props, const char *key, size_t key_len) { - if (!props) return NULL; - for (size_t i = 0; i < props->v.obj.len; i++) { - ScrDynEntry *e = &props->v.obj.entries[i]; - if (e->key_len == key_len && !memcmp(e->key, key, key_len)) return e; - } - return NULL; +/* Own property `key` of the table object, in *out; false when absent. */ +static bool hp_entry(const ScrDyn *props, const char *key, size_t key_len, ScrDynProp *out) { + if (!props) return false; + size_t i = scr_dyn_obj_index_of(props, key, key_len); + if (i == SIZE_MAX) return false; + *out = scr_dyn_obj_prop_at(props, i); + return true; } static bool hp_has(const ScrDyn *props, const char *key, size_t key_len) { - return hp_entry(props, key, key_len) != NULL; + return props && scr_dyn_obj_index_of(props, key, key_len) != SIZE_MAX; } /* A property the program added, read with the child as `this`: an @@ -78,10 +78,10 @@ static bool hp_has(const ScrDyn *props, const char *key, size_t key_len) { * it (`Object.defineProperty(child, "x", { get() { return this.pid; } })`). * +1, or NULL with the exception pending. */ static ScrDyn *hp_read(ScrDyn *props, ScrDyn *self, const char *key, size_t key_len) { - ScrDynEntry *e = hp_entry(props, key, key_len); - if (!e || !e->accessor) return scr_dyn_obj_read(props, key, key_len); - if (!e->getter) return scr_dyn_retain(scr_dyn_undefined()); - ScrDyn *getter = scr_dyn_retain(e->getter); + ScrDynProp e; + if (!hp_entry(props, key, key_len, &e) || !e.accessor) return scr_dyn_obj_read(props, key, key_len); + if (!e.getter) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *getter = scr_dyn_retain(e.getter); scr_dyn_this_push_dyn(self); ScrDyn *result = scr_dyn_call(getter, NULL, 0, "getter"); scr_dyn_this_pop(); @@ -132,7 +132,8 @@ static ScrDyn *hp_get(void *h, const char *key, size_t key_len) { static bool hp_set(void *h, const char *key, size_t key_len, const ScrDyn *value) { ScrDyn *props = hp_find(h); - ScrDynEntry *entry = hp_entry(props, key, key_len); + ScrDynProp found; + const ScrDynProp *entry = hp_entry(props, key, key_len, &found) ? &found : NULL; if (entry && entry->accessor) { /* An accessor's setter runs on the child. Without one the write throws * the strict-mode TypeError, as the other handle getters do. */ diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index d28707d5d..1b30c451d 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -4821,14 +4821,14 @@ static ScrArr *sf_headers_convert_init(const ScrDyn *headers, bool *canonical_ou return converted; } if (headers->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < headers->v.obj.len; i++) { - const ScrDynEntry *e = &headers->v.obj.entries[i]; - ScrStr *raw_name = scr_str_new(e->key, e->key_len); + for (size_t i = 0; i < scr_dyn_obj_count(headers); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(headers, i); + ScrStr *raw_name = scr_str_new(e.key, e.key_len); ScrStr *name_bytes = sf_header_value_bytestring(raw_name); ScrStr *name = name_bytes ? sf_latin1_to_utf8(name_bytes) : NULL; scr_str_release(name_bytes); scr_str_release(raw_name); - ScrDyn *source = scr_dyn_retain(e->value); + ScrDyn *source = scr_dyn_retain(e.value); ScrStr *raw_value = name ? scr_dyn_string_coerce_js(source) : NULL; scr_dyn_release(source); ScrStr *value_bytes = raw_value @@ -6088,11 +6088,11 @@ static int8_t sf_light_header_form(const ScrDyn *headers, bool json_body, } int8_t form = 0; if (headers->kind == SCR_DYN_OBJ) { - ScrArr *out = scr_arr_new(SCR_ELEM_STR, 2 * headers->v.obj.len); + ScrArr *out = scr_arr_new(SCR_ELEM_STR, 2 * scr_dyn_obj_count(headers)); form = 3; - for (size_t i = 0; i < headers->v.obj.len && form == 3; i++) { - const ScrDynEntry *e = &headers->v.obj.entries[i]; - const ScrDyn *value = e->value; + for (size_t i = 0; i < scr_dyn_obj_count(headers) && form == 3; i++) { + ScrDynProp e = scr_dyn_obj_prop_at(headers, i); + const ScrDyn *value = e.value; size_t count = value->kind == SCR_DYN_ARR ? value->v.arr.len : 1; for (size_t j = 0; j < count; j++) { const ScrDyn *item = value->kind == SCR_DYN_ARR ? value->v.arr.items[j] : value; @@ -6106,7 +6106,7 @@ static int8_t sf_light_header_form(const ScrDyn *headers, bool json_body, form = 0; break; } - scr_arr_push_ref(out, scr_str_new(e->key, e->key_len)); + scr_arr_push_ref(out, scr_str_new(e.key, e.key_len)); scr_arr_push_ref(out, text); } } @@ -6666,27 +6666,27 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) { return sf_reject_now(promise, "fetch failed"); } if (init && init->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < init->v.obj.len; i++) { - const ScrDynEntry *entry = &init->v.obj.entries[i]; - bool supported = sf_name(entry->key, entry->key_len, "method") || - sf_name(entry->key, entry->key_len, "headers") || - sf_name(entry->key, entry->key_len, "body") || - sf_name(entry->key, entry->key_len, "duplex") || - sf_name(entry->key, entry->key_len, "redirect") || - sf_name(entry->key, entry->key_len, "signal"); + for (size_t i = 0; i < scr_dyn_obj_count(init); i++) { + ScrDynProp entry = scr_dyn_obj_prop_at(init, i); + bool supported = sf_name(entry.key, entry.key_len, "method") || + sf_name(entry.key, entry.key_len, "headers") || + sf_name(entry.key, entry.key_len, "body") || + sf_name(entry.key, entry.key_len, "duplex") || + sf_name(entry.key, entry.key_len, "redirect") || + sf_name(entry.key, entry.key_len, "signal"); bool recognized_unsupported = - sf_name(entry->key, entry->key_len, "cache") || - sf_name(entry->key, entry->key_len, "credentials") || - sf_name(entry->key, entry->key_len, "dispatcher") || - sf_name(entry->key, entry->key_len, "integrity") || - sf_name(entry->key, entry->key_len, "keepalive") || - sf_name(entry->key, entry->key_len, "mode") || - sf_name(entry->key, entry->key_len, "priority") || - sf_name(entry->key, entry->key_len, "referrer") || - sf_name(entry->key, entry->key_len, "referrerPolicy") || - sf_name(entry->key, entry->key_len, "window"); - if (!supported && recognized_unsupported && entry->value && - entry->value->kind != SCR_DYN_UNDEF) { + sf_name(entry.key, entry.key_len, "cache") || + sf_name(entry.key, entry.key_len, "credentials") || + sf_name(entry.key, entry.key_len, "dispatcher") || + sf_name(entry.key, entry.key_len, "integrity") || + sf_name(entry.key, entry.key_len, "keepalive") || + sf_name(entry.key, entry.key_len, "mode") || + sf_name(entry.key, entry.key_len, "priority") || + sf_name(entry.key, entry.key_len, "referrer") || + sf_name(entry.key, entry.key_len, "referrerPolicy") || + sf_name(entry.key, entry.key_len, "window"); + if (!supported && recognized_unsupported && entry.value && + entry.value->kind != SCR_DYN_UNDEF) { return sf_reject_now(promise, "fetch failed"); } } diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index 3fb0fdcba..0284c31c4 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -2047,10 +2047,10 @@ void scr_http_res_write_head_dyn(ScrHttpRes *r, double status, const ScrDyn *hea scr_dyn_arg_type_fail("headers", "an instance of Object", headers); return; } - for (size_t i = 0; i < headers->v.obj.len; i++) { - const ScrDynEntry *e = &headers->v.obj.entries[i]; - const ScrDyn *v = e->value; - ScrStr *name = scr_str_new(e->key, e->key_len); + for (size_t i = 0; i < scr_dyn_obj_count(headers); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(headers, i); + const ScrDyn *v = e.value; + ScrStr *name = scr_str_new(e.key, e.key_len); if (v->kind == SCR_DYN_STR) { scr_http_res_set_header(r, name, v->v.str); } else if (v->kind == SCR_DYN_NUM) { @@ -6200,10 +6200,10 @@ static bool scr_http_dynh_req_set(void *h, const char *key, size_t key_len, cons * consecutive same-name pairs). Numbers format through String(n). NULL = * a value this surface cannot honestly carry (exception pending). */ static ScrArr *scr_http_dynh_pairs(const ScrDyn *headers) { - ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, headers->v.obj.len * 2); - for (size_t i = 0; i < headers->v.obj.len; i++) { - const ScrDynEntry *e = &headers->v.obj.entries[i]; - const ScrDyn *v = e->value; + ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, scr_dyn_obj_count(headers) * 2); + for (size_t i = 0; i < scr_dyn_obj_count(headers); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(headers, i); + const ScrDyn *v = e.value; size_t reps = v->kind == SCR_DYN_ARR ? v->v.arr.len : 1; for (size_t k = 0; k < reps; k++) { const ScrDyn *one = v->kind == SCR_DYN_ARR ? v->v.arr.items[k] : v; @@ -6218,7 +6218,7 @@ static ScrArr *scr_http_dynh_pairs(const ScrDyn *headers) { scr_jb_puts(&b, "Invalid value \""); scr_jb_puts(&b, one->kind == SCR_DYN_UNDEF ? "undefined" : "object"); scr_jb_puts(&b, "\" for header \""); - for (size_t c = 0; c < e->key_len; c++) scr_jb_putc(&b, e->key[c]); + for (size_t c = 0; c < e.key_len; c++) scr_jb_putc(&b, e.key[c]); scr_jb_putc(&b, '"'); ScrStr *msg = scr_jb_finish(&b); scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_HTTP_INVALID_HEADER_VALUE"); @@ -6226,7 +6226,7 @@ static ScrArr *scr_http_dynh_pairs(const ScrDyn *headers) { scr_arr_release(pairs); return NULL; } - scr_arr_push_ref(pairs, scr_str_new(e->key, e->key_len)); + scr_arr_push_ref(pairs, scr_str_new(e.key, e.key_len)); scr_arr_push_ref(pairs, vs); } } @@ -6945,8 +6945,8 @@ static ScrDyn *scr_http_dynh_agent_get(void *h, const char *key, size_t key_len) scr_dyn_arr_push(arr, item); } /* the list is most recent first: reverse each array into Node's order */ - for (size_t i = 0; i < obj->v.obj.len; i++) { - ScrDyn *arr = obj->v.obj.entries[i].value; + for (size_t i = 0; i < scr_dyn_obj_count(obj); i++) { + ScrDyn *arr = scr_dyn_obj_value_at(obj, i); for (size_t l = 0, r = arr->v.arr.len ? arr->v.arr.len - 1 : 0; l < r; l++, r--) { ScrDyn *t = arr->v.arr.items[l]; arr->v.arr.items[l] = arr->v.arr.items[r]; @@ -7449,13 +7449,13 @@ bool scr_http_req_opts_read(const ScrDyn *opts, bool secure, const char *api, Sc headers = headers_view; } if (headers != NULL && headers->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < headers->v.obj.len; i++) { - const ScrDynEntry *e = &headers->v.obj.entries[i]; - ScrStr *name = scr_str_new(e->key, e->key_len); + for (size_t i = 0; i < scr_dyn_obj_count(headers); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(headers, i); + ScrStr *name = scr_str_new(e.key, e.key_len); ScrStr *label = scr_str_new("", 0); scr_http_validate_header_name(name, label); scr_str_release(label); - const ScrDyn *v = e->value; + const ScrDyn *v = e.value; if (!scr_exc_pending()) { if (v->kind == SCR_DYN_ARR) { for (size_t k = 0; k < v->v.arr.len && !scr_exc_pending(); k++) diff --git a/packages/runtime/src/scr_http2.c b/packages/runtime/src/scr_http2.c index 489946a12..30fddf8a4 100644 --- a/packages/runtime/src/scr_http2.c +++ b/packages/runtime/src/scr_http2.c @@ -2559,9 +2559,9 @@ ScrNetServer *scr_http2_create_secure_server_dyn(const ScrDyn *opts /*borrowed*/ ScrHttpReqFn fn) { bool allow_http1 = false; if (opts != NULL && opts->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < opts->v.obj.len; i++) { - if (strcmp(opts->v.obj.entries[i].key, "allowHTTP1") == 0) { - allow_http1 = scr_dyn_truthy(opts->v.obj.entries[i].value); + for (size_t i = 0; i < scr_dyn_obj_count(opts); i++) { + if (strcmp(scr_dyn_obj_prop_at(opts, i).key, "allowHTTP1") == 0) { + allow_http1 = scr_dyn_truthy(scr_dyn_obj_value_at(opts, i)); } } } @@ -3105,15 +3105,15 @@ static void scr_h2_dyn_fire_goaway(ScrClosure *cb, double code, double last) { /* Flatten a dyn headers object into the flat pairs array (numbers via * String(n); the :status/:method/... pseudo-headers pass through). */ static ScrArr *scr_h2_dom_to_pairs(const ScrDyn *headers) { - ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, headers->v.obj.len * 2); - for (size_t i = 0; i < headers->v.obj.len; i++) { - const ScrDynEntry *e = &headers->v.obj.entries[i]; - const ScrDyn *v = e->value; + ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, scr_dyn_obj_count(headers) * 2); + for (size_t i = 0; i < scr_dyn_obj_count(headers); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(headers, i); + const ScrDyn *v = e.value; ScrStr *vs; if (v->kind == SCR_DYN_STR) vs = scr_str_retain(v->v.str); else if (v->kind == SCR_DYN_NUM) vs = scr_f64_to_scrstr(v->v.num); else continue; /* undefined/null header: dropped, Node's stance */ - scr_arr_push_ref(pairs, scr_str_new(e->key, e->key_len)); + scr_arr_push_ref(pairs, scr_str_new(e.key, e.key_len)); scr_arr_push_ref(pairs, vs); } return pairs; diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 1de583af5..b4994cd32 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -973,21 +973,23 @@ ScrStr *scr_insp_regex(ScrRegex *re) { * the k-th symbol key. */ static size_t insp_view_property_slots(const ScrDyn *props) { if (!props) return 0; - return props->v.obj.len + (props->symbol_keys ? props->symbol_keys->v.arr.len : 0); + return scr_dyn_obj_count(props) + (props->symbol_keys ? props->symbol_keys->v.arr.len : 0); } -static ScrStr *insp_accessor_tag(const ScrDynEntry *ent); +static ScrStr *insp_accessor_tag(const ScrDynProp *ent); static void insp_key_into(InspBuf *b, const char *key, size_t len); /* Slot k's `key: value` entry when it is enumerable, else NULL. */ static ScrStr *insp_view_property(ScrDyn *props, size_t k, double recurse, double depth) { - const ScrDynEntry *ent; + ScrDynProp found; + const ScrDynProp *ent = NULL; ScrStr *key = NULL; - if (k < props->v.obj.len) { - ent = &props->v.obj.entries[k]; + if (k < scr_dyn_obj_count(props)) { + found = scr_dyn_obj_prop_at(props, k); + ent = &found; } else { - ScrDyn *symbol = props->symbol_keys->v.arr.items[k - props->v.obj.len]; - ent = scr_dyn_own_symbol_entry(props, symbol); + ScrDyn *symbol = props->symbol_keys->v.arr.items[k - scr_dyn_obj_count(props)]; + if (scr_dyn_own_symbol_prop(props, symbol, &found)) ent = &found; if (ent && ent->enumerable) key = scr_insp_dyn(symbol, recurse, depth); } if (!ent || !ent->enumerable) return NULL; @@ -1005,10 +1007,11 @@ static ScrStr *insp_view_property(ScrDyn *props, size_t k, double recurse, doubl static size_t insp_view_property_count(ScrDyn *props) { size_t count = 0; for (size_t k = 0; k < insp_view_property_slots(props); k++) { - if (k < props->v.obj.len) count += props->v.obj.entries[k].enumerable; + if (k < scr_dyn_obj_count(props)) count += scr_dyn_obj_prop_at(props, k).enumerable; else { - const ScrDynEntry *ent = scr_dyn_own_symbol_entry(props, props->symbol_keys->v.arr.items[k - props->v.obj.len]); - count += ent && ent->enumerable; + ScrDynProp ent; + count += scr_dyn_own_symbol_prop(props, props->symbol_keys->v.arr.items[k - scr_dyn_obj_count(props)], &ent) && + ent.enumerable; } } return count; @@ -1157,11 +1160,11 @@ static ScrDyn *insp_error_cause(ScrError *e, bool *enumerable, ScrDyn **view) { ScrDyn *node = scr_errdyn_view_of(e); if (node != NULL && node->kind == SCR_DYN_TYPED_REF) node = *view = scr_dyn_typed_ref_materialize(node); if (node != NULL && node->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < node->v.obj.len; i++) { - ScrDynEntry *ent = &node->v.obj.entries[i]; - if (ent->key_len == 5 && memcmp(ent->key, "cause", 5) == 0) { - *enumerable = ent->enumerable; - return ent->value; + for (size_t i = 0; i < scr_dyn_obj_count(node); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(node, i); + if (ent.key_len == 5 && memcmp(ent.key, "cause", 5) == 0) { + *enumerable = ent.enumerable; + return ent.value; } } return NULL; @@ -1176,9 +1179,9 @@ static ScrDyn *insp_error_cause(ScrError *e, bool *enumerable, ScrDyn **view) { * other non-enumerable property of an error. */ bool scr_insp_error_bag_has_properties(ScrDyn *bag) { if (bag == NULL || bag->kind != SCR_DYN_OBJ) return false; - for (size_t i = 0; i < bag->v.obj.len; i++) { - const ScrDynEntry *ent = &bag->v.obj.entries[i]; - if (ent->enumerable && !(ent->key_len == 5 && memcmp(ent->key, "cause", 5) == 0)) return true; + for (size_t i = 0; i < scr_dyn_obj_count(bag); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(bag, i); + if (ent.enumerable && !(ent.key_len == 5 && memcmp(ent.key, "cause", 5) == 0)) return true; } return false; } @@ -1282,7 +1285,7 @@ ScrStr *scr_insp_error(ScrError *e, double recurse, double depth) { * (/^[a-zA-Z_][a-zA-Z_0-9]*$/, '__proto__' excepted), quoted otherwise. */ /* util.inspect's text for an accessor property: [Getter], [Setter] or * [Getter/Setter], by which of the two are functions. +1. */ -static ScrStr *insp_accessor_tag(const ScrDynEntry *ent) { +static ScrStr *insp_accessor_tag(const ScrDynProp *ent) { bool get = ent->getter && ent->getter->kind == SCR_DYN_FUNC; bool set = ent->setter && ent->setter->kind == SCR_DYN_FUNC; const char *text = get && set ? "[Getter/Setter]" : get ? "[Getter]" : set ? "[Setter]" : "undefined"; @@ -1371,12 +1374,12 @@ static ScrDyn *insp_assertion_ellipsis(ScrDyn *value) { * Node's `{ ... }` tail. */ /* An own non-enumerable entry of the error encoding, NULL when absent or * enumerable (an enumerable one prints with the other own keys). */ -static const ScrDynEntry *insp_error_hidden_entry(const ScrDyn *d, const char *key, size_t len) { - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *ent = &d->v.obj.entries[i]; - if (ent->key_len == len && memcmp(ent->key, key, len) == 0) return ent->enumerable ? NULL : ent; - } - return NULL; +static const ScrDynProp *insp_error_hidden_entry(const ScrDyn *d, const char *key, size_t len, + ScrDynProp *out) { + size_t i = scr_dyn_obj_index_of(d, key, len); + if (i == SIZE_MAX) return NULL; + *out = scr_dyn_obj_prop_at(d, i); + return out->enumerable ? NULL : out; } static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { @@ -1390,19 +1393,20 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { memcmp(name->v.str->data, "AssertionError", 14) == 0; double value_depth = assertion ? recurse : depth; size_t extra = 0; - for (size_t i = 0; i < d->v.obj.len; i++) { - ScrDynEntry *ent = &d->v.obj.entries[i]; - if (!ent->enumerable) continue; - if ((ent->key_len == 4 && memcmp(ent->key, "name", 4) == 0) || - (ent->key_len == 7 && memcmp(ent->key, "message", 7) == 0) || - (ent->key_len == 5 && memcmp(ent->key, "stack", 5) == 0)) + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(d, i); + if (!ent.enumerable) continue; + if ((ent.key_len == 4 && memcmp(ent.key, "name", 4) == 0) || + (ent.key_len == 7 && memcmp(ent.key, "message", 7) == 0) || + (ent.key_len == 5 && memcmp(ent.key, "stack", 5) == 0)) continue; extra++; } /* formatError shows a non-enumerable `cause`, and an AggregateError's * `errors` array, after the enumerable keys: `[cause]`, `[errors]`. */ - const ScrDynEntry *cause = insp_error_hidden_entry(d, "cause", 5); - const ScrDynEntry *errors = insp_error_hidden_entry(d, "errors", 6); + ScrDynProp cause_prop, errors_prop; + const ScrDynProp *cause = insp_error_hidden_entry(d, "cause", 5, &cause_prop); + const ScrDynProp *errors = insp_error_hidden_entry(d, "errors", 6, &errors_prop); if (errors && errors->value->kind != SCR_DYN_ARR) errors = NULL; extra += (cause != NULL) + (errors != NULL); InspBuf base = {0}; @@ -1426,20 +1430,20 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { if (extra == 0) return base_str; scr_insp_begin(recurse + 1); scr_insp_seen_push(d); - for (size_t i = 0; i < d->v.obj.len; i++) { - ScrDynEntry *ent = &d->v.obj.entries[i]; - if (!ent->enumerable) continue; - if ((ent->key_len == 4 && memcmp(ent->key, "name", 4) == 0) || - (ent->key_len == 7 && memcmp(ent->key, "message", 7) == 0) || - (ent->key_len == 5 && memcmp(ent->key, "stack", 5) == 0)) + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(d, i); + if (!ent.enumerable) continue; + if ((ent.key_len == 4 && memcmp(ent.key, "name", 4) == 0) || + (ent.key_len == 7 && memcmp(ent.key, "message", 7) == 0) || + (ent.key_len == 5 && memcmp(ent.key, "stack", 5) == 0)) continue; - bool shortened = assertion && ((ent->key_len == 6 && memcmp(ent->key, "actual", 6) == 0) || - (ent->key_len == 8 && memcmp(ent->key, "expected", 8) == 0)); - ScrDyn *shown = shortened ? insp_assertion_ellipsis(ent->value) : scr_dyn_retain(ent->value); + bool shortened = assertion && ((ent.key_len == 6 && memcmp(ent.key, "actual", 6) == 0) || + (ent.key_len == 8 && memcmp(ent.key, "expected", 8) == 0)); + ScrDyn *shown = shortened ? insp_assertion_ellipsis(ent.value) : scr_dyn_retain(ent.value); ScrStr *val = scr_insp_dyn(shown, recurse + 1, value_depth); scr_dyn_release(shown); InspBuf eb = {0}; - insp_key_into(&eb, ent->key, ent->key_len); + insp_key_into(&eb, ent.key, ent.key_len); ib_cstr(&eb, ": "); ib_bytes(&eb, val->data, val->len); scr_str_release(val); @@ -1447,7 +1451,7 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { scr_insp_entry(entry, false); scr_str_release(entry); } - const ScrDynEntry *hidden[] = {cause, errors}; + const ScrDynProp *hidden[] = {cause, errors}; for (size_t i = 0; i < 2; i++) { if (!hidden[i]) continue; ScrStr *val = scr_insp_dyn(hidden[i]->value, recurse + 1, value_depth); @@ -1690,9 +1694,9 @@ static ScrStr *scr_insp_dyn_collection(ScrDyn *d, double recurse, double depth, /* An own symbol-keyed property of `d` (borrowed) this render shows, or * NULL: enumerable ones, and under showHidden the non-enumerable ones too. */ -static ScrDynEntry *scr_insp_symbol_entry(ScrDyn *d, size_t i) { - ScrDynEntry *ent = scr_dyn_own_symbol_entry(d, d->symbol_keys->v.arr.items[i]); - return ent && (ent->enumerable || g_show_hidden) ? ent : NULL; +static bool scr_insp_symbol_entry(ScrDyn *d, size_t i, ScrDynProp *out) { + return scr_dyn_own_symbol_prop(d, d->symbol_keys->v.arr.items[i], out) && + (out->enumerable || g_show_hidden); } /* An ordinary object: `Name { key: value }` with its constructor name @@ -1700,15 +1704,21 @@ static ScrDynEntry *scr_insp_symbol_entry(ScrDyn *d, size_t i) { * enumerable string keys, then the own enumerable symbol keys. */ /* An own entry this render shows: enumerable ones, and under showHidden * the non-enumerable ones too (never the encoding's `%` markers). */ -static bool insp_obj_shows(const ScrDynEntry *ent) { +static bool insp_obj_shows(const ScrDynProp *ent) { return ent->enumerable || (g_show_hidden && !(ent->key_len && ent->key[0] == '%')); } static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char *cls) { size_t shown = 0; - for (size_t i = 0; i < d->v.obj.len; i++) shown += insp_obj_shows(&d->v.obj.entries[i]); + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(d, i); + shown += insp_obj_shows(&ent); + } size_t nsymbols = d->symbol_keys ? d->symbol_keys->v.arr.len : 0; - for (size_t i = 0; i < nsymbols; i++) shown += scr_insp_symbol_entry(d, i) != NULL; + for (size_t i = 0; i < nsymbols; i++) { + ScrDynProp ent; + shown += scr_insp_symbol_entry(d, i, &ent); + } if (shown == 0) { if (cls) return scr_insp_class_tag(cls, " {}"); return d->null_proto ? scr_str_new("[Object: null prototype] {}", 27) @@ -1720,17 +1730,17 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char : insp_styled_lit("[Object]", 8, INSP_SPECIAL); } scr_insp_begin(recurse + 1); - for (size_t i = 0; i < d->v.obj.len; i++) { - ScrDynEntry *ent = &d->v.obj.entries[i]; - if (!insp_obj_shows(ent)) continue; - ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse + 1, depth); + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(d, i); + if (!insp_obj_shows(&ent)) continue; + ScrStr *val = ent.accessor ? insp_accessor_tag(&ent) : scr_insp_dyn(ent.value, recurse + 1, depth); InspBuf eb = {0}; - if (ent->enumerable) { - insp_key_into(&eb, ent->key, ent->key_len); + if (ent.enumerable) { + insp_key_into(&eb, ent.key, ent.key_len); } else { /* formatProperty's hidden key: the key's rendering in brackets. */ ib_char(&eb, '['); - insp_key_into(&eb, ent->key, ent->key_len); + insp_key_into(&eb, ent.key, ent.key_len); ib_char(&eb, ']'); } ib_cstr(&eb, ": "); @@ -1742,8 +1752,9 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char if (scr_exc_pending()) break; } for (size_t i = 0; i < nsymbols; i++) { - ScrDynEntry *ent = scr_insp_symbol_entry(d, i); - if (!ent) continue; + ScrDynProp found; + if (!scr_insp_symbol_entry(d, i, &found)) continue; + const ScrDynProp *ent = &found; ScrStr *key = scr_insp_dyn(d->symbol_keys->v.arr.items[i], recurse + 1, depth); ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse + 1, depth); InspBuf eb = {0}; @@ -2063,7 +2074,7 @@ static ScrDyn *insp_inspect_value(void) { ScrDyn *set = scr_dyn_new_func(scr_closure_new((void *)members[i], 0), insp_inspect_member_call, 1, "native:function", members[i]); scr_dyn_obj_set_accessor(table, members[i], strlen(members[i]), get, set); - table->v.obj.entries[table->v.obj.len - 1].enumerable = false; + scr_dyn_obj_set_attrs_at(table, scr_dyn_obj_count(table) - 1, true, false, true); } scr_dyn_release(table); } @@ -2100,15 +2111,15 @@ static ScrDyn *insp_user_options(double depth) { insp_opt_set(options, "getters", scr_dyn_new_bool(false)); insp_opt_set(options, "numericSeparator", scr_dyn_new_bool(false)); if (g_user_options && g_user_options->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < g_user_options->v.obj.len; i++) { - const ScrDynEntry *ent = &g_user_options->v.obj.entries[i]; - if (!ent->enumerable) continue; - ScrDyn *value = scr_dyn_obj_read((ScrDyn *)g_user_options, ent->key, ent->key_len); + for (size_t i = 0; i < scr_dyn_obj_count(g_user_options); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(g_user_options, i); + if (!ent.enumerable) continue; + ScrDyn *value = scr_dyn_obj_read((ScrDyn *)g_user_options, ent.key, ent.key_len); if (!value) { scr_dyn_release(options); return NULL; } - scr_dyn_obj_set(options, ent->key, ent->key_len, value); + scr_dyn_obj_set(options, ent.key, ent.key_len, value); } } return options; @@ -2343,9 +2354,12 @@ static ScrStr *scr_insp_dyn_func(ScrDyn *d, double recurse, double depth) { } size_t shown = 0; if (table && table->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < table->v.obj.len; i++) shown += table->v.obj.entries[i].enumerable; + for (size_t i = 0; i < scr_dyn_obj_count(table); i++) shown += scr_dyn_obj_prop_at(table, i).enumerable; size_t nsymbols = table->symbol_keys ? table->symbol_keys->v.arr.len : 0; - for (size_t i = 0; i < nsymbols; i++) shown += scr_insp_symbol_entry(table, i) != NULL; + for (size_t i = 0; i < nsymbols; i++) { + ScrDynProp ent; + shown += scr_insp_symbol_entry(table, i, &ent); + } } if (shown == 0) { scr_dyn_release(table); @@ -2498,7 +2512,10 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { * own `length` comes first, and non-enumerable properties show too. */ const ScrDyn *props = d->v.arr.properties; size_t nprops = 0; - for (size_t i = 0; props && i < props->v.obj.len; i++) nprops += insp_obj_shows(&props->v.obj.entries[i]); + for (size_t i = 0; props && i < scr_dyn_obj_count(props); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(props, i); + nprops += insp_obj_shows(&ent); + } /* An instance of a class that extends Array: Node's `Name(length) ` * prefix (scr_arr_copy_metadata names the class). */ const char *cls = props ? props->v.obj.class_name : NULL; @@ -2562,15 +2579,15 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { scr_str_release(s); entries++; } - for (size_t k = 0; props && k < props->v.obj.len; k++) { - const ScrDynEntry *ent = &props->v.obj.entries[k]; - if (!insp_obj_shows(ent)) continue; - ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse + 1, depth); + for (size_t k = 0; props && k < scr_dyn_obj_count(props); k++) { + ScrDynProp ent = scr_dyn_obj_prop_at(props, k); + if (!insp_obj_shows(&ent)) continue; + ScrStr *val = ent.accessor ? insp_accessor_tag(&ent) : scr_insp_dyn(ent.value, recurse + 1, depth); InspBuf eb = {0}; /* formatProperty's hidden key: the key's rendering in brackets. */ - if (!ent->enumerable) ib_char(&eb, '['); - insp_key_into(&eb, ent->key, ent->key_len); - if (!ent->enumerable) ib_char(&eb, ']'); + if (!ent.enumerable) ib_char(&eb, '['); + insp_key_into(&eb, ent.key, ent.key_len); + if (!ent.enumerable) ib_char(&eb, ']'); ib_cstr(&eb, ": "); ib_bytes(&eb, val->data, val->len); scr_str_release(val); @@ -2753,8 +2770,8 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { scr_str_release(iso); ScrStr *prefix = ib_take(&head); bool fields = false; - for (size_t i = 0; i < materialized->v.obj.len && !fields; i++) - fields = materialized->v.obj.entries[i].enumerable; + for (size_t i = 0; i < scr_dyn_obj_count(materialized) && !fields; i++) + fields = scr_dyn_obj_prop_at(materialized, i).enumerable; if (!fields || recurse > depth) { scr_dyn_release(materialized); return prefix; @@ -3323,12 +3340,12 @@ bool scr_insp_format_read_options(const ScrDyn *options, ScrInspFormatOptions *o "maxArrayLength", "maxStringLength", "breakLength", "compact", "sorted", "getters", "numericSeparator", "stylize", }; - for (size_t i = 0; i < options->v.obj.len && !o->user_options; i++) { - const ScrDynEntry *ent = &options->v.obj.entries[i]; - if (!ent->enumerable) continue; + for (size_t i = 0; i < scr_dyn_obj_count(options) && !o->user_options; i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(options, i); + if (!ent.enumerable) continue; bool read = false; for (size_t k = 0; k < sizeof known / sizeof *known && !read; k++) - read = ent->key_len == strlen(known[k]) && memcmp(ent->key, known[k], ent->key_len) == 0; + read = ent.key_len == strlen(known[k]) && memcmp(ent.key, known[k], ent.key_len) == 0; if (!read) o->user_options = options; } } @@ -3436,10 +3453,10 @@ static ScrStr *insp_table_cell(ScrDyn *v) { } const char *base = v->null_proto ? "[Object: null prototype]" : "Object"; size_t n = 0; - for (size_t i = 0; i < v->v.obj.len; i++) { - ScrDynEntry *ent = &v->v.obj.entries[i]; - if (!ent->enumerable) continue; - if (ent->accessor) { + for (size_t i = 0; i < scr_dyn_obj_count(v); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(v, i); + if (!ent.enumerable) continue; + if (ent.accessor) { static const char message[] = "console.table cells holding accessor properties have no lowering"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return scr_str_new("", 0); @@ -3463,12 +3480,12 @@ static ScrStr *insp_table_cell(ScrDyn *v) { } ScrStr *items[2]; size_t count = 0; - for (size_t i = 0; i < v->v.obj.len; i++) { - ScrDynEntry *ent = &v->v.obj.entries[i]; - if (!ent->enumerable) continue; - ScrStr *val = scr_insp_dyn(ent->value, 1, 0); + for (size_t i = 0; i < scr_dyn_obj_count(v); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(v, i); + if (!ent.enumerable) continue; + ScrStr *val = scr_insp_dyn(ent.value, 1, 0); InspBuf eb = {0}; - insp_key_into(&eb, ent->key, ent->key_len); + insp_key_into(&eb, ent.key, ent.key_len); ib_cstr(&eb, ": "); ib_str(&eb, val); scr_str_release(val); diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 928400da3..0b780603b 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -716,8 +716,9 @@ ScrDyn *scr_process_versions(void) { scr_dyn_obj_set(scr_versions_object, "openssl", 7, scr_dyn_new_str(openssl)); scr_str_release(node); scr_str_release(openssl); - for (size_t i = 0; i < scr_versions_object->v.obj.len; i++) { - scr_versions_object->v.obj.entries[i].writable = false; + for (size_t i = 0; i < scr_dyn_obj_count(scr_versions_object); i++) { + ScrDynProp prop = scr_dyn_obj_prop_at(scr_versions_object, i); + scr_dyn_obj_set_attrs_at(scr_versions_object, i, false, prop.enumerable, prop.configurable); } scr_atexit(&scr_versions_object_cleanup); } @@ -914,8 +915,8 @@ static void scr_env_store(ScrDyn *out, const char *text, size_t ks, size_t ke, } static int scr_env_entry_order(const void *left, const void *right) { - const ScrDynEntry *a = *(const ScrDynEntry *const *)left; - const ScrDynEntry *b = *(const ScrDynEntry *const *)right; + const ScrDynProp *a = left; + const ScrDynProp *b = right; size_t n = a->key_len < b->key_len ? a->key_len : b->key_len; int cmp = memcmp(a->key, b->key, n); return cmp ? cmp : (a->key_len > b->key_len) - (a->key_len < b->key_len); @@ -991,14 +992,14 @@ static ScrDyn *scr_env_parse_content(const ScrStr *input, bool ordinary) { /* Node's std::map orders UTF-8 keys before installing object properties. * Ordinary objects ignore string-valued __proto__ assignments; loading * the native environment retains that name as an ordinary variable. */ - size_t count = entries->v.obj.len; - const ScrDynEntry **sorted = count ? malloc(count * sizeof(*sorted)) : NULL; + size_t count = scr_dyn_obj_count(entries); + ScrDynProp *sorted = count ? malloc(count * sizeof(*sorted)) : NULL; if (count && !sorted) scr_trap("scriptc: out of memory\n"); - for (size_t i = 0; i < count; i++) sorted[i] = &entries->v.obj.entries[i]; + for (size_t i = 0; i < count; i++) sorted[i] = scr_dyn_obj_prop_at(entries, i); if (count > 1) qsort(sorted, count, sizeof(*sorted), scr_env_entry_order); ScrDyn *result = ordinary ? scr_dyn_new_obj() : scr_dyn_new_obj_null_proto(); for (size_t i = 0; i < count; i++) { - const ScrDynEntry *entry = sorted[i]; + const ScrDynProp *entry = &sorted[i]; if (ordinary && entry->key_len == 9 && memcmp(entry->key, "__proto__", 9) == 0) continue; if (!ordinary) { scr_dyn_obj_set(result, entry->key, entry->key_len, scr_dyn_retain(entry->value)); @@ -1103,15 +1104,15 @@ void scr_process_load_env_file(const ScrDyn *value) { if (!content) return; ScrDyn *entries = scr_env_parse_content(content, false); scr_str_release(content); - for (size_t i = 0; i < entries->v.obj.len; i++) { - const ScrDynEntry *entry = &entries->v.obj.entries[i]; + for (size_t i = 0; i < scr_dyn_obj_count(entries); i++) { + ScrDynProp entry = scr_dyn_obj_prop_at(entries, i); /* Node normalizes each raw sorted entry immediately before checking * the environment. Distinct invalid byte keys can normalize alike; * the first absent name wins, rather than the last parsed entry. */ - ScrStr *key = scr_str_from_utf8_lossy((const uint8_t *)entry->key, entry->key_len); + ScrStr *key = scr_str_from_utf8_lossy((const uint8_t *)entry.key, entry.key_len); ScrStr *existing = scr_env_get(key); if (!existing) { - const ScrStr *raw = entry->value->v.str; + const ScrStr *raw = entry.value->v.str; ScrStr *value = scr_str_from_utf8_lossy((const uint8_t *)raw->data, raw->len); scr_env_set(key, value); scr_str_release(value); @@ -2810,10 +2811,9 @@ ScrDyn *scr_process_release(void) { #endif #undef SCR_RELEASE_FILE #undef SCR_RELEASE_PREFIX - for (size_t i = 0; i < scr_release_object->v.obj.len; i++) { - scr_release_object->v.obj.entries[i].writable = false; - scr_release_object->v.obj.entries[i].configurable = false; - } + for (size_t i = 0; i < scr_dyn_obj_count(scr_release_object); i++) + scr_dyn_obj_set_attrs_at(scr_release_object, i, false, + scr_dyn_obj_prop_at(scr_release_object, i).enumerable, false); scr_atexit(&scr_release_object_cleanup); } return scr_dyn_retain(scr_release_object); diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c index d4eb263cf..419dcea4b 100644 --- a/packages/runtime/src/scr_message.c +++ b/packages/runtime/src/scr_message.c @@ -636,9 +636,9 @@ static bool scr_publish_container_ok(const ScrDyn *d) { if (!scr_dyn_is_frozen(d)) return false; if (d->kind == SCR_DYN_ARR) return d->v.arr.properties == NULL; if (d->null_proto || d->v.obj.source_identity || d->v.obj.source_access) return false; - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *e = &d->v.obj.entries[i]; - if (e->accessor || !e->enumerable) return false; + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(d, i); + if (e.accessor || !e.enumerable) return false; } return true; } @@ -666,8 +666,8 @@ static bool scr_publish_collect(ScrPublish *p, ScrDyn *root) { break; case SCR_DYN_OBJ: if (!scr_publish_container_ok(d)) return false; - for (size_t i = 0; i < d->v.obj.len; i++) { - ScrDyn *child = d->v.obj.entries[i].value; + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDyn *child = scr_dyn_obj_value_at(d, i); if (child && !scr_publish_seen(p, child)) scr_publish_push(p, child); } break; @@ -1033,14 +1033,15 @@ ScrDyn *scr_message_decode(const ScrMessage *message) { } else scr_dyn_obj_set(values[i], property->key, property->length, value); } if (node->kind == SCR_MESSAGE_ERROR) { - for (size_t j = 0; j < values[i]->v.obj.len; j++) { - ScrDynEntry *entry = &values[i]->v.obj.entries[j]; - entry->enumerable = false; + for (size_t j = 0; j < scr_dyn_obj_count(values[i]); j++) { + ScrDynProp entry = scr_dyn_obj_prop_at(values[i], j); + bool enumerable = false; for (size_t k = 0; k < node->count; k++) { const ScrMessageProperty *property = &node->properties[k]; - if (!property->hidden && property->length == entry->key_len && !memcmp(property->key, entry->key, entry->key_len)) - entry->enumerable = true; + if (!property->hidden && property->length == entry.key_len && !memcmp(property->key, entry.key, entry.key_len)) + enumerable = true; } + scr_dyn_obj_set_attrs_at(values[i], j, entry.writable, enumerable, entry.configurable); } ScrError *error = scr_errdyn_err_of(values[i]); scr_error_commit_dyn(error, values[i]); diff --git a/packages/runtime/src/scr_os.c b/packages/runtime/src/scr_os.c index 8bc476045..637bb7e61 100644 --- a/packages/runtime/src/scr_os.c +++ b/packages/runtime/src/scr_os.c @@ -745,9 +745,8 @@ double scr_os_cpus_field(double i, double field) { static void scr_os_constant(ScrDyn *o, const char *name, double value) { scr_dyn_obj_set(o, name, strlen(name), scr_dyn_new_num(value)); - ScrDynEntry *entry = &o->v.obj.entries[o->v.obj.len - 1]; - entry->writable = false; - entry->configurable = false; + size_t last = scr_dyn_obj_count(o) - 1; + scr_dyn_obj_set_attrs_at(o, last, false, scr_dyn_obj_prop_at(o, last).enumerable, false); } #define SCR_OS_CONSTANT(o, name) scr_os_constant((o), #name, (double)(name)) @@ -1514,10 +1513,10 @@ ScrDyn *scr_node_constants(const ScrDyn *crypto_table) { scr_os_signal_constants(o); scr_fs_constants(o); if (crypto && crypto->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < crypto->v.obj.len; i++) { - const ScrDynEntry *entry = &crypto->v.obj.entries[i]; - if (entry->accessor || !entry->value) continue; - scr_dyn_obj_set(o, entry->key, entry->key_len, scr_dyn_retain(entry->value)); + for (size_t i = 0; i < scr_dyn_obj_count(crypto); i++) { + ScrDynProp entry = scr_dyn_obj_prop_at(crypto, i); + if (entry.accessor || !entry.value) continue; + scr_dyn_obj_set(o, entry.key, entry.key_len, scr_dyn_retain(entry.value)); } } scr_dyn_release(crypto); @@ -1718,10 +1717,12 @@ double scr_util_signal_exit_code(const ScrDyn *code) { ScrJsonBuf reason; scr_jb_init(&reason); scr_jb_puts(&reason, "must be one of: "); - for (size_t i = 0; i < signals->v.obj.len; i++) { + for (size_t i = 0; i < scr_dyn_obj_count(signals); i++) { if (i > 0) scr_jb_puts(&reason, ", "); scr_jb_putc(&reason, '\''); - scr_jb_write(&reason, signals->v.obj.entries[i].key, signals->v.obj.entries[i].key_len); + size_t key_len; + const char *key = scr_dyn_obj_key_at(signals, i, &key_len); + scr_jb_write(&reason, key, key_len); scr_jb_putc(&reason, '\''); } ScrStr *text = scr_jb_finish(&reason); diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index 8d11d3806..1ba2b22cd 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -278,8 +278,8 @@ static bool scr_pub_dyn_ok(const ScrDyn *d, const char **why) { *why = "Cannot publish an Object object"; return false; } - for (size_t i = 0; i < d->v.obj.len; i++) - if (d->v.obj.entries[i].accessor) { + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) + if (scr_dyn_obj_prop_at(d, i).accessor) { *why = "Cannot publish an accessor property"; return false; } @@ -325,9 +325,9 @@ void scr_pub_dyn_fn(void *v, ScrPub *ctx) { case SCR_DYN_ARR: case SCR_DYN_OBJ: { if (!scr_pub_dyn_ok(d, &why)) break; - size_t n = d->kind == SCR_DYN_ARR ? d->v.arr.len : d->v.obj.len; + size_t n = d->kind == SCR_DYN_ARR ? d->v.arr.len : scr_dyn_obj_count(d); for (size_t i = 0; i < n; i++) { - ScrDyn *child = d->kind == SCR_DYN_ARR ? d->v.arr.items[i] : d->v.obj.entries[i].value; + ScrDyn *child = d->kind == SCR_DYN_ARR ? d->v.arr.items[i] : scr_dyn_obj_value_at(d, i); if (child && child->rc != SIZE_MAX) SCR_PUB_ADD((uintptr_t)child); } break; diff --git a/packages/runtime/src/scr_region.c b/packages/runtime/src/scr_region.c index 25bbcf7ae..dacd9f163 100644 --- a/packages/runtime/src/scr_region.c +++ b/packages/runtime/src/scr_region.c @@ -858,12 +858,12 @@ static void scr_rgn_walk_dyn(ScrRgnWalk *w, ScrDyn *d) { break; case SCR_DYN_OBJ: /* source_identity is owned through its access callback: not walked. */ - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *e = &d->v.obj.entries[i]; - if (e->accessor) { - scr_rgn_enter(w, e->getter, SCR_RGN_W_DYN, NULL); - scr_rgn_enter(w, e->setter, SCR_RGN_W_DYN, NULL); - } else scr_rgn_enter(w, e->value, SCR_RGN_W_DYN, NULL); + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(d, i); + if (e.accessor) { + scr_rgn_enter(w, e.getter, SCR_RGN_W_DYN, NULL); + scr_rgn_enter(w, e.setter, SCR_RGN_W_DYN, NULL); + } else scr_rgn_enter(w, e.value, SCR_RGN_W_DYN, NULL); } break; case SCR_DYN_FUNC: scr_rgn_enter(w, d->v.fn.clo, SCR_RGN_W_CLOSURE, NULL); break; diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index 7e573fc28..372e60702 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -333,7 +333,7 @@ static void scr_dirent_init(void) { scr_dyn_obj_set(proto, methods[i].name, strlen(methods[i].name), scr_dyn_new_func(scr_closure_new(NULL, 0), methods[i].thunk, 0, "native:fs.Dirent", methods[i].name)); - proto->v.obj.entries[proto->v.obj.len - 1].enumerable = false; + scr_dyn_obj_set_attrs_at(proto, scr_dyn_obj_count(proto) - 1, true, false, true); } ScrStr *desc = scr_str_new("type", 4); ScrSym *sym = scr_sym_new(desc); @@ -350,15 +350,14 @@ void scr_dyn_dirent_finish(ScrDyn *obj) { scr_dyn_region_store(obj, NULL); /* a prototype store (ScrDynRegion) */ scr_dyn_release(obj->prototype); obj->prototype = scr_dyn_retain(scr_dirent_prototype); - for (size_t i = 0; i < obj->v.obj.len; i++) { - ScrDynEntry *ent = &obj->v.obj.entries[i]; - if (ent->key_len != 6 || memcmp(ent->key, "%dtype", 6) != 0) continue; - ent->enumerable = false; + size_t i = scr_dyn_obj_index_of(obj, "%dtype", 6); + if (i != SIZE_MAX) { + ScrDynProp ent = scr_dyn_obj_prop_at(obj, i); + scr_dyn_obj_set_attrs_at(obj, i, ent.writable, false, ent.configurable); if (!obj->symbol_properties && !obj->symbol_keys) - scr_dyn_symbol_init_own(obj, scr_dirent_type_key, scr_dirent_type_storage, ent->value); + scr_dyn_symbol_init_own(obj, scr_dirent_type_key, scr_dirent_type_storage, ent.value); else - scr_dyn_symbol_key_set(obj, scr_dirent_type_key, ent->value); - break; + scr_dyn_symbol_key_set(obj, scr_dirent_type_key, ent.value); } } diff --git a/packages/runtime/src/scr_tls.c b/packages/runtime/src/scr_tls.c index 547fef27a..76ec48e6c 100644 --- a/packages/runtime/src/scr_tls.c +++ b/packages/runtime/src/scr_tls.c @@ -2597,21 +2597,21 @@ static bool scr_tls_srv_opts_walk_ex(const ScrDyn *opts, const char *api, ScrByt } if (!scr_tls_opts_validate(opts)) return false; /* Node's typed errors first */ bool ok = true; - for (size_t i = 0; ok && i < opts->v.obj.len; i++) { - const ScrDynEntry *e = &opts->v.obj.entries[i]; - const ScrDyn *v = e->value; + for (size_t i = 0; ok && i < scr_dyn_obj_count(opts); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(opts, i); + const ScrDyn *v = e.value; if (v->kind == SCR_DYN_UNDEF) continue; /* an undefined member reads as absent, like Node */ - if (strcmp(e->key, "cert") == 0 || strcmp(e->key, "key") == 0) { + if (strcmp(e.key, "cert") == 0 || strcmp(e.key, "key") == 0) { char what[96]; - snprintf(what, sizeof what, "a %s '%s' option", api, e->key); + snprintf(what, sizeof what, "a %s '%s' option", api, e.key); ScrBytes *b = scr_tls_pem_dyn(v, what, false); if (b == NULL) { ok = false; break; } - ScrBytes **slot = e->key[0] == 'c' ? cert_out : key_out; + ScrBytes **slot = e.key[0] == 'c' ? cert_out : key_out; scr_bytes_release(*slot); *slot = b; continue; } - if (strcmp(e->key, "ca") == 0) { + if (strcmp(e.key, "ca") == 0) { /* the server's `ca` verifies client certificates and completes the * server's own chain (scr_tls_srv_client_auth); a walk without * client certificates drops it */ @@ -2623,7 +2623,7 @@ static bool scr_tls_srv_opts_walk_ex(const ScrDyn *opts, const char *api, ScrByt } continue; } - if (strcmp(e->key, "requestCert") == 0) { + if (strcmp(e.key, "requestCert") == 0) { if (auth != NULL) { auth->request = scr_tls_dyn_truthy(v); continue; @@ -2635,22 +2635,22 @@ static bool scr_tls_srv_opts_walk_ex(const ScrDyn *opts, const char *api, ScrByt } continue; } - if (strcmp(e->key, "rejectUnauthorized") == 0) { + if (strcmp(e.key, "rejectUnauthorized") == 0) { if (auth != NULL) auth->reject = scr_tls_dyn_not_false(v); continue; /* without client certificates it has no effect */ } - if (server_ok && (strcmp(e->key, "ALPNProtocols") == 0 || strcmp(e->key, "minVersion") == 0 || - strcmp(e->key, "maxVersion") == 0)) + if (server_ok && (strcmp(e.key, "ALPNProtocols") == 0 || strcmp(e.key, "minVersion") == 0 || + strcmp(e.key, "maxVersion") == 0)) continue; - if (strcmp(e->key, "SNICallback") == 0) { + if (strcmp(e.key, "SNICallback") == 0) { scr_tls_opt_fence(api, "SNICallback", "SNICallback lowers only on http2.createSecureServer({ allowHTTP1: true, ... }) with a literal options object"); ok = false; continue; } for (size_t k = 0; SCR_TLS_SRV_FENCED_OPTIONS[k] != NULL; k++) { - if (strcmp(e->key, SCR_TLS_SRV_FENCED_OPTIONS[k]) == 0) { - scr_tls_opt_fence(api, e->key, + if (strcmp(e.key, SCR_TLS_SRV_FENCED_OPTIONS[k]) == 0) { + scr_tls_opt_fence(api, e.key, "cert and key (PEM strings or Buffers) are the honestly-implemented members of a runtime options record"); ok = false; break; @@ -2957,17 +2957,17 @@ static const char *const SCR_TLS_WRAP_FENCED_OPTIONS[] = { * certificates are not modeled), SNICallback and ALPNCallback fence. * False with the exception pending. */ static bool scr_tls_wrap_socket_opts_check(const ScrDyn *record, const char *api) { - for (size_t i = 0; record != NULL && i < record->v.obj.len; i++) { - const ScrDynEntry *e = &record->v.obj.entries[i]; - if (e->value->kind == SCR_DYN_UNDEF) continue; - if (strcmp(e->key, "requestCert") == 0) { - if (!scr_tls_dyn_truthy(e->value)) continue; + for (size_t i = 0; record != NULL && i < scr_dyn_obj_count(record); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(record, i); + if (e.value->kind == SCR_DYN_UNDEF) continue; + if (strcmp(e.key, "requestCert") == 0) { + if (!scr_tls_dyn_truthy(e.value)) continue; scr_tls_opt_fence(api, "requestCert", "requestCert lowers with cert and key options, not beside a secureContext"); return false; } - if (strcmp(e->key, "SNICallback") == 0 || strcmp(e->key, "ALPNCallback") == 0) { - scr_tls_opt_fence(api, e->key, + if (strcmp(e.key, "SNICallback") == 0 || strcmp(e.key, "ALPNCallback") == 0) { + scr_tls_opt_fence(api, e.key, "isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members"); return false; } @@ -2980,21 +2980,21 @@ ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrow static const char api[] = "new tls.TLSSocket"; bool is_server = false; const ScrDyn *record = opts != NULL && opts->kind == SCR_DYN_OBJ ? opts : NULL; - for (size_t i = 0; record != NULL && i < record->v.obj.len; i++) { - const ScrDynEntry *e = &record->v.obj.entries[i]; - if (e->value->kind == SCR_DYN_UNDEF) continue; - if (strcmp(e->key, "isServer") == 0) { - is_server = scr_tls_dyn_truthy(e->value); + for (size_t i = 0; record != NULL && i < scr_dyn_obj_count(record); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(record, i); + if (e.value->kind == SCR_DYN_UNDEF) continue; + if (strcmp(e.key, "isServer") == 0) { + is_server = scr_tls_dyn_truthy(e.value); continue; } - if (strcmp(e->key, "secureContext") == 0) { + if (strcmp(e.key, "secureContext") == 0) { scr_tls_opt_fence(api, "secureContext", "secureContext lowers as a member of an options object literal"); return NULL; } for (size_t k = 0; SCR_TLS_WRAP_FENCED_OPTIONS[k] != NULL; k++) { - if (strcmp(e->key, SCR_TLS_WRAP_FENCED_OPTIONS[k]) == 0) { - scr_tls_opt_fence(api, e->key, + if (strcmp(e.key, SCR_TLS_WRAP_FENCED_OPTIONS[k]) == 0) { + scr_tls_opt_fence(api, e.key, "isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members"); return NULL; } @@ -3267,10 +3267,10 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, /* createSecureContext's version checks come before its other options * (Node's SecureContext constructor). */ if (ok && !scr_tls_versions_dyn(opts, &min_version, &max_version)) ok = false; - for (size_t i = 0; ok && i < opts->v.obj.len; i++) { - const ScrDynEntry *e = &opts->v.obj.entries[i]; - const ScrDyn *v = e->value; - if (strcmp(e->key, "checkServerIdentity") == 0) { + for (size_t i = 0; ok && i < scr_dyn_obj_count(opts); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(opts, i); + const ScrDyn *v = e.value; + if (strcmp(e.key, "checkServerIdentity") == 0) { /* Node spreads user options over the defaults, so a PRESENT key * replaces the builtin verifier even when its value is undefined * — validateFunction then throws for anything non-callable. A @@ -3287,7 +3287,7 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, continue; } if (v->kind == SCR_DYN_UNDEF) continue; - if (strcmp(e->key, "port") == 0) { + if (strcmp(e.key, "port") == 0) { if (port >= 0) continue; /* the argument form wins, like Node */ if (v->kind == SCR_DYN_NUM) port = v->v.num; else if (v->kind == SCR_DYN_STR) port = strtod(v->v.str->data, NULL); /* Node coerces numeric strings */ @@ -3297,7 +3297,7 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, } continue; } - if (strcmp(e->key, "host") == 0) { + if (strcmp(e.key, "host") == 0) { if (v->kind == SCR_DYN_STR) opt_host = v->v.str; else { scr_dyn_arg_type_fail("options.host", "of type string", v); @@ -3305,34 +3305,34 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, } continue; } - if (strcmp(e->key, "rejectUnauthorized") == 0) { + if (strcmp(e.key, "rejectUnauthorized") == 0) { reject = scr_tls_dyn_not_false(v); continue; } - if (strcmp(e->key, "ca") == 0) { + if (strcmp(e.key, "ca") == 0) { scr_bytes_release(ca); if (!scr_tls_ca_dyn(v, "a tls.connect 'ca' option", &ca)) ok = false; continue; } - if (strcmp(e->key, "cert") == 0 || strcmp(e->key, "key") == 0) { - ScrBytes **slot = e->key[0] == 'c' ? &own_cert : &own_key; + if (strcmp(e.key, "cert") == 0 || strcmp(e.key, "key") == 0) { + ScrBytes **slot = e.key[0] == 'c' ? &own_cert : &own_key; scr_bytes_release(*slot); - *slot = scr_tls_pem_dyn(v, e->key[0] == 'c' ? "a tls.connect 'cert' option" : "a tls.connect 'key' option", + *slot = scr_tls_pem_dyn(v, e.key[0] == 'c' ? "a tls.connect 'cert' option" : "a tls.connect 'key' option", false); if (*slot == NULL) ok = false; continue; } - if (strcmp(e->key, "ALPNProtocols") == 0) { + if (strcmp(e.key, "ALPNProtocols") == 0) { alpn_value = v; /* converted after the context options, as TLSSocket does */ continue; } - if (strcmp(e->key, "minVersion") == 0 || strcmp(e->key, "maxVersion") == 0) continue; - if (strcmp(e->key, "timeout") == 0) { + if (strcmp(e.key, "minVersion") == 0 || strcmp(e.key, "maxVersion") == 0) continue; + if (strcmp(e.key, "timeout") == 0) { /* `if (options.timeout) tlssock.setTimeout(options.timeout)` */ timeout_value = scr_tls_dyn_truthy(v) ? v : NULL; continue; } - if (strcmp(e->key, "socket") == 0) { + if (strcmp(e.key, "socket") == 0) { if (v->kind == SCR_DYN_HANDLE && v->v.handle.tag == SCR_DYNH_NET_SOCKET) { wrap = (ScrNetSocket *)v->v.handle.ptr; } else { @@ -3341,7 +3341,7 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, } continue; } - if (strcmp(e->key, "servername") == 0) { + if (strcmp(e.key, "servername") == 0) { if (v->kind == SCR_DYN_STR) servername = v->v.str; else { scr_dyn_arg_type_fail("options.servername", "of type string", v); @@ -3350,8 +3350,8 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, continue; } for (size_t k = 0; SCR_TLS_CONNECT_FENCED_OPTIONS[k] != NULL; k++) { - if (strcmp(e->key, SCR_TLS_CONNECT_FENCED_OPTIONS[k]) == 0) { - scr_tls_opt_fence("tls.connect", e->key, + if (strcmp(e.key, SCR_TLS_CONNECT_FENCED_OPTIONS[k]) == 0) { + scr_tls_opt_fence("tls.connect", e.key, "port/host/socket/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout are the implemented members of a runtime options record"); ok = false; break; diff --git a/packages/runtime/src/scr_util_compare.c b/packages/runtime/src/scr_util_compare.c index 5a15b662f..4c2cb9d16 100644 --- a/packages/runtime/src/scr_util_compare.c +++ b/packages/runtime/src/scr_util_compare.c @@ -26,15 +26,15 @@ static bool util_deep_equal(const ScrDyn *, const ScrDyn *, bool, const ScrDeepF static size_t util_deep_key_count(const ScrDyn *table) { size_t count = 0; - if (table) for (size_t i = 0; i < table->v.obj.len; i++) count += table->v.obj.entries[i].enumerable; + if (table) for (size_t i = 0; i < scr_dyn_obj_count(table); i++) count += scr_dyn_obj_prop_at(table, i).enumerable; return count; } static bool util_deep_same_keys(const ScrDyn *a, const ScrDyn *b) { if (util_deep_key_count(a) != util_deep_key_count(b)) return false; - if (a) for (size_t i = 0; i < a->v.obj.len; i++) { - const ScrDynEntry *e = &a->v.obj.entries[i]; - if (e->enumerable && (!b || !scr_dyn_obj_enumerable(b, e->key, e->key_len))) return false; + if (a) for (size_t i = 0; i < scr_dyn_obj_count(a); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(a, i); + if (e.enumerable && (!b || !scr_dyn_obj_enumerable(b, e.key, e.key_len))) return false; } return true; } @@ -42,14 +42,14 @@ static bool util_deep_same_keys(const ScrDyn *a, const ScrDyn *b) { static ScrDyn *util_deep_constructor(const ScrDyn *value) { for (const ScrDyn *p = value; p; p = p->prototype) { const ScrDyn *table = p->kind == SCR_DYN_OBJ ? p : p->kind == SCR_DYN_ARR ? p->v.arr.properties : NULL; - if (table) for (size_t i = 0; i < table->v.obj.len; i++) { - const ScrDynEntry *e = &table->v.obj.entries[i]; - if (e->key_len == 11 && !memcmp(e->key, "constructor", 11)) { - if (e->accessor) { util_deep_refused("native accessor properties"); return NULL; } - if (p == value && e->value->kind == SCR_DYN_FUNC) { + if (table) for (size_t i = 0; i < scr_dyn_obj_count(table); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(table, i); + if (e.key_len == 11 && !memcmp(e.key, "constructor", 11)) { + if (e.accessor) { util_deep_refused("native accessor properties"); return NULL; } + if (p == value && e.value->kind == SCR_DYN_FUNC) { util_deep_refused("native own constructor functions"); return NULL; } - return scr_dyn_retain(e->value); + return scr_dyn_retain(e.value); } } if (!p->prototype) { @@ -77,8 +77,8 @@ static ScrDyn *util_deep_tag(const ScrDyn *value) { if (!scr_dyn_same_value(keys->v.arr.items[i], key)) continue; // General native getters retain an explicit boundary, including // non-enumerable Symbol.toStringTag descriptors read by Node. - for (size_t j = 0; j < p->symbol_properties->v.obj.len; j++) { - if (p->symbol_properties->v.obj.entries[j].accessor) { + for (size_t j = 0; j < scr_dyn_obj_count(p->symbol_properties); j++) { + if (scr_dyn_obj_prop_at(p->symbol_properties, j).accessor) { scr_dyn_release(key); util_deep_refused("native accessor properties"); return NULL; } } @@ -95,20 +95,21 @@ static bool util_deep_properties(const ScrDyn *a, const ScrDyn *b, bool skip, const ScrDeepFrame *frame) { if (util_deep_key_count(a) != util_deep_key_count(b)) return false; if (!a) return true; - for (size_t i = 0; i < a->v.obj.len; i++) { - const ScrDynEntry *x = &a->v.obj.entries[i]; - if (!x->enumerable) continue; - const ScrDynEntry *y = NULL; - if (b) for (size_t j = 0; j < b->v.obj.len; j++) { - const ScrDynEntry *candidate = &b->v.obj.entries[j]; - if (candidate->enumerable && candidate->key_len == x->key_len && - !memcmp(candidate->key, x->key, x->key_len)) { y = candidate; break; } + for (size_t i = 0; i < scr_dyn_obj_count(a); i++) { + ScrDynProp x = scr_dyn_obj_prop_at(a, i); + if (!x.enumerable) continue; + ScrDynProp yp; + const ScrDynProp *y = NULL; + if (b) for (size_t j = 0; j < scr_dyn_obj_count(b); j++) { + ScrDynProp candidate = scr_dyn_obj_prop_at(b, j); + if (candidate.enumerable && candidate.key_len == x.key_len && + !memcmp(candidate.key, x.key, x.key_len)) { yp = candidate; y = &yp; break; } } if (!y) return false; - if (x->accessor || y->accessor) return util_deep_refused("native accessor properties"); + if (x.accessor || y->accessor) return util_deep_refused("native accessor properties"); // Materializing a cyclic typed child can refresh its ancestor table. // Retain the values before that refresh replaces descriptor storage. - ScrDyn *xv = scr_dyn_retain(x->value), *yv = scr_dyn_retain(y->value); + ScrDyn *xv = scr_dyn_retain(x.value), *yv = scr_dyn_retain(y->value); bool same = util_deep_equal(xv, yv, skip, frame); scr_dyn_release(xv); scr_dyn_release(yv); if (!same) return false; diff --git a/packages/runtime/src/scr_worker_events.c b/packages/runtime/src/scr_worker_events.c index a23395c91..7669c4fb9 100644 --- a/packages/runtime/src/scr_worker_events.c +++ b/packages/runtime/src/scr_worker_events.c @@ -1385,17 +1385,17 @@ static ScrDyn *scr_worker_new_options(double root, ScrStr *filename, ScrDyn *opt scr_dyn_arg_type_fail("options", "object", options); return NULL; } - for (size_t i = 0; i < options->v.obj.len; i++) { - const ScrDynEntry *entry = &options->v.obj.entries[i]; - if (entry->accessor) return scr_worker_refusal("Worker option accessors are not supported yet"); - if (entry->key_len == 10 && !memcmp(entry->key, "workerData", 10)) data = entry->value; - else if (entry->key_len == 4 && !memcmp(entry->key, "argv", 4)) argv = entry->value; - else if (entry->key_len == 12 && !memcmp(entry->key, "transferList", 12)) transfer_list = entry->value; - else if (entry->key_len == 14 && !memcmp(entry->key, "resourceLimits", 14)) limits = entry->value; - else if (entry->key_len == 4 && !memcmp(entry->key, "name", 4)) name = entry->value; - else if (entry->key_len == 6 && !memcmp(entry->key, "stdout", 6)) capture |= scr_dyn_truthy(entry->value) ? 1 : 0; - else if (entry->key_len == 6 && !memcmp(entry->key, "stderr", 6)) capture |= scr_dyn_truthy(entry->value) ? 2 : 0; - else if (entry->key_len == 5 && !memcmp(entry->key, "stdin", 5)) capture |= scr_dyn_truthy(entry->value) ? 4 : 0; + for (size_t i = 0; i < scr_dyn_obj_count(options); i++) { + ScrDynProp entry = scr_dyn_obj_prop_at(options, i); + if (entry.accessor) return scr_worker_refusal("Worker option accessors are not supported yet"); + if (entry.key_len == 10 && !memcmp(entry.key, "workerData", 10)) data = entry.value; + else if (entry.key_len == 4 && !memcmp(entry.key, "argv", 4)) argv = entry.value; + else if (entry.key_len == 12 && !memcmp(entry.key, "transferList", 12)) transfer_list = entry.value; + else if (entry.key_len == 14 && !memcmp(entry.key, "resourceLimits", 14)) limits = entry.value; + else if (entry.key_len == 4 && !memcmp(entry.key, "name", 4)) name = entry.value; + else if (entry.key_len == 6 && !memcmp(entry.key, "stdout", 6)) capture |= scr_dyn_truthy(entry.value) ? 1 : 0; + else if (entry.key_len == 6 && !memcmp(entry.key, "stderr", 6)) capture |= scr_dyn_truthy(entry.value) ? 2 : 0; + else if (entry.key_len == 5 && !memcmp(entry.key, "stdin", 5)) capture |= scr_dyn_truthy(entry.value) ? 4 : 0; else return scr_worker_refusal("This Worker constructor option is not supported yet"); } } diff --git a/packages/runtime/test/object-core.test.ts b/packages/runtime/test/object-core.test.ts new file mode 100644 index 000000000..04f7cfb38 --- /dev/null +++ b/packages/runtime/test/object-core.test.ts @@ -0,0 +1,23 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test } from "vitest"; + +const src = join(import.meta.dirname, "..", "src"); + +/* Ordinary objects keep their properties behind the object core: units + * outside it read and adjust them through the scr_dyn.h own-property + * accessors (scr_dyn_obj_count, scr_dyn_obj_prop_at, ...), so the core can + * change how it stores them. */ +const CORE = new Set(["scr_json.c", "scr_js_object.c", "scr_dyn.h"]); + +test("only the object core reads ordinary objects' entry storage", () => { + const offenders: string[] = []; + for (const name of readdirSync(src)) { + if (!/\.(c|h)$/.test(name) || name.endsWith(".test.c") || CORE.has(name)) continue; + const lines = readFileSync(join(src, name), "utf8").split("\n"); + lines.forEach((line, i) => { + if (/\bv\.obj\.(entries|len)\b/.test(line)) offenders.push(`${name}:${i + 1}: ${line.trim()}`); + }); + } + expect(offenders).toEqual([]); +}); From dc1e8acc1d6afc84b38d4f7abf836b74dc3b3c04 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Mon, 26 Oct 2026 08:28:59 -0700 Subject: [PATCH 2/2] Store ordinary objects as a layout plus value slots A checked object (SCR_DYN_OBJ) now starts in layout mode: v.obj.layout names its key sequence, a node of the thread's interned transition tree, and v.obj.slots holds one 8-byte value per key, where every property used to take a 48-byte entry. All properties of a layout-mode object are writable, enumerable, configurable data properties. An accessor, a non-default attribute, a delete, more than 64 keys, spent layout budgets, publication to other threads, or any code that reads entries directly moves the object to dictionary mode (today's entries) for good, so unported paths stay correct. Layouts replace the dynamic tier's lazily computed hidden classes (ScrJsShape) and keep their budgets. Inline caches key on the layout pointer and read the slot directly; JSON.parse builds objects along layouts; stringify, key enumeration, release, trace and the runtime read cache read slots in place. The emitted index-signature checks read members through runtime accessors. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/backend/llvm/dyn.ts | 35 +- packages/compiler/src/backend/llvm/js-ic.ts | 83 +- .../compiler/src/backend/native/executable.ts | 2 +- packages/compiler/src/ir/runtime-features.ts | 2 +- .../runtime-pack-matrix.mjs | 6 +- packages/runtime/src/scr_dyn.h | 169 +++- packages/runtime/src/scr_js.h | 48 +- packages/runtime/src/scr_js_object.c | 297 ++----- packages/runtime/src/scr_js_object.test.c | 11 +- packages/runtime/src/scr_js_object.test.ts | 2 +- packages/runtime/src/scr_json.c | 768 +++++++++++++----- packages/runtime/src/scr_json.test.c | 15 +- packages/runtime/src/scr_message.c | 7 +- packages/runtime/src/scr_message.test.c | 7 +- packages/runtime/src/scr_publish.c | 6 +- packages/runtime/test/object-core.test.ts | 5 +- packages/runtime/test/test_json.c | 8 +- 17 files changed, 882 insertions(+), 589 deletions(-) diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 12ea99688..d797dd5b2 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -25,10 +25,9 @@ import { InternalCompilerError } from "../../errors.js"; * are selected from the target C ABI. * v.num double | v.b i8 | v.str/v.bytes ptr at +16; * v.arr { len +16, cap +24, items +32 }; - * v.obj { len +16, cap +24, entries +32 }; + * v.obj { len +16 } (own properties; read them through + * scr_dyn_member_*, which know both object modes); * v.fn { clo +16, thunk +24, sig +32, name +40, arity +48 }. - * ScrDynEntry { key, key_len, value, getter, setter, accessor, writable, - * enumerable, configurable } — 48 bytes on 64-bit, 24 on 32-bit. * ScrDynKind: NULL=0 BOOL=1 NUM=2 STR=3 ARR=4 OBJ=5 UNDEF=6 BYTES=7 * FUNC=8 HANDLE=9. * ScrBytes { rc +0; len +8; elem +16; data +24 }. @@ -842,27 +841,23 @@ export class LlDyn { return e; } - /** Object entry field addresses: entries + i*sizeof(ScrDynEntry), with - * key, key_len, and value at offsets 0, 8, and 16 on 64-bit targets. */ + /** Own property i of object `d` (borrowed key bytes, key length and + * value), read through the runtime's accessors: the object may be in + * layout or dictionary mode (scr_dyn.h). */ private entryAt( B: BlockBuilder, - entries: string, + d: string, i: string, ): { key: string; keyLen: string; value: string } { - const off = B.tmp(); - const base = B.tmp(); - B.line(`${off} = mul ${this.S} ${i}, ${this.abiOffset(48, 24)} ; sizeof(ScrDynEntry)`); - B.line(`${base} = getelementptr inbounds i8, ptr ${entries}, ${this.S} ${off}`); + this.host.declare(`declare ptr @scr_dyn_member_key(ptr, ${this.S})`); + this.host.declare(`declare ${this.S} @scr_dyn_member_key_len(ptr, ${this.S})`); + this.host.declare(`declare ptr @scr_dyn_member_value(ptr, ${this.S})`); const key = B.tmp(); - B.line(`${key} = load ptr, ptr ${base}`); - const klp = B.tmp(); const keyLen = B.tmp(); - B.line(`${klp} = getelementptr inbounds i8, ptr ${base}, i64 ${this.abiOffset(8, 4)}`); - B.line(`${keyLen} = load ${this.S}, ptr ${klp}`); - const vp = B.tmp(); const value = B.tmp(); - B.line(`${vp} = getelementptr inbounds i8, ptr ${base}, i64 ${this.abiOffset(16, 8)}`); - B.line(`${value} = load ptr, ptr ${vp}`); + B.line(`${key} = call ptr @scr_dyn_member_key(ptr ${d}, ${this.S} ${i})`); + B.line(`${keyLen} = call ${this.S} @scr_dyn_member_key_len(ptr ${d}, ${this.S} ${i})`); + B.line(`${value} = call ptr @scr_dyn_member_value(ptr ${d}, ${this.S} ${i})`); return { key, keyLen, value }; } @@ -1434,9 +1429,8 @@ export class LlDyn { if (shape.indexValue && shape.indexValue.kind !== "dyn") { this.host.declare(`declare i32 @memcmp(ptr, ptr, ${this.S})`); const n = this.lenOf(B, "%d"); - const entries = this.itemsOf(B, "%d"); this.i64Loop(B, "dm.ov", n, (i, brNext) => { - const ent = this.entryAt(B, entries, i); + const ent = this.entryAt(B, "%d", i); // Skip declared keys. for (const f of shape.fields) { const klen = Buffer.byteLength(f.name, "utf8"); @@ -2770,9 +2764,8 @@ export class LlDyn { B.line(`store i8 ${np}, ptr ${flagsp}`); } const n = this.lenOf(B, "%d"); - const entries = this.itemsOf(B, "%d"); this.i64Loop(B, "dcv", n, (i, brNext) => { - const ent = this.entryAt(B, entries, i); + const ent = this.entryAt(B, "%d", i); for (const f of shape.fields) { const klen = Buffer.byteLength(f.name, "utf8"); const lenEq = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/js-ic.ts b/packages/compiler/src/backend/llvm/js-ic.ts index d51b838bb..dc1aeab16 100644 --- a/packages/compiler/src/backend/llvm/js-ic.ts +++ b/packages/compiler/src/backend/llvm/js-ic.ts @@ -4,13 +4,12 @@ * A property site in a dynamic-tier function owns one zeroed thread-local * ScrJsIc record (`@sc_js_ic_N`). The code at the site is fixed: * - * load ordinary object whose shape is entry 0's own-property shape: - * read entry `slot` inline unless it is an accessor; otherwise - * scr_js_get_probe (polymorphic, prototype hits, absent keys, - * misses) - * store ordinary object whose shape is entry 0's replace shape, outside - * an intact parse region, writable data entry: replace inline; - * otherwise scr_js_store_probe (adds, polymorphic, misses) + * load ordinary object whose layout is entry 0's own-property layout: + * read slot `slot` inline; otherwise scr_js_get_probe + * (polymorphic, prototype hits, absent keys, misses) + * store ordinary object whose layout is entry 0's replace layout, + * outside an intact parse region: replace inline; otherwise + * scr_js_store_probe (adds, polymorphic, misses) * keyed dense array element at an integral number key: inline; * otherwise scr_js_keyed_probe (string keys of ordinary objects) * method dynInvoke's method read: scr_js_load_probe @@ -22,12 +21,13 @@ * defines the operation's semantics; nothing is ever patched. * * Layout facts (64-bit only; 32-bit targets keep the helper calls): - * ScrDyn kind +8; v.obj {len +16, entries +32, region +72, - * shape +80}; v.arr {len +16, items +32, presence +56}; - * v.num +16 - * ScrDynEntry 48 bytes: value +16, accessor bit 0 of +40, writable +41 - * ScrJsIc entries of 40 bytes {shape, target, holder, holder_shape, - * slot}; 168 bytes in all */ + * ScrDyn kind +8; v.obj {len +16, slots +32, region +72, + * layout +80}; v.arr {len +16, items +32, presence +56}; + * v.num +16. Every property of an object with a layout is + * a writable data property (scr_dyn.h's layout mode); + * dictionary-mode objects have a null layout. + * ScrJsIc entries of 40 bytes {layout, target, holder, + * holder_layout, slot}; 168 bytes in all */ import { BOOL, DYN, F64, STRING, type IrExpr, type IrFunction } from "../../ir/ir.js"; import { jsIcMethodSites, type JsIcSite } from "../../ir/js-ic.js"; import { mangleBorrowedFunction } from "../mangle.js"; @@ -42,8 +42,6 @@ type Host = LlvmEmitterContext; /** Words of one ScrJsIc record. */ export const JS_IC_WORDS = 21; -const ENTRY_STRIDE = 48; - /** The cache site a call is, when it is emitted in a dynamic-tier function * of a 64-bit target. */ export function jsIcSiteOf(host: Host, e: ExprOf<"call">): JsIcSite | null { @@ -88,32 +86,17 @@ function kindIs(host: Host, d: string, kind: number, yes: string, no: string): v * the prototype hit made method-heavy code 3-6% faster for 15-90 bytes. * So a site tests entry 0, own or prototype, and its probe does the rest. * (0 makes every access probe: with a runtime built with SCR_JS_VERIFY, - * that checks every cached shape against its object.) */ + * that probes every access.) */ const INLINE_WAYS: number = 1; const INLINE_PROTO = true; -/** The address of entry `slot` of object `d`'s entries. */ -function entryAt(host: Host, d: string, slot: string): string { +/** The address of slot `slot` of layout-mode object `d`. */ +function slotAt(host: Host, d: string, slot: string): string { const B = host.B; - const entries = load(host, "ptr", d, 32); - const offset = B.tmp(); - const entry = B.tmp(); - B.line(`${offset} = mul i64 ${slot}, ${ENTRY_STRIDE}`); - B.line(`${entry} = getelementptr inbounds i8, ptr ${entries}, i64 ${offset}`); - return entry; -} - -/** Branches to `miss` when the entry at `entry` is an accessor. */ -function dataEntry(host: Host, entry: string, miss: string): void { - const B = host.B; - const flags = load(host, "i8", entry, 40); - const accessor = B.tmp(); - const isAccessor = B.tmp(); - B.line(`${accessor} = and i8 ${flags}, 1`); - B.line(`${isAccessor} = icmp ne i8 ${accessor}, 0`); - const data = B.newLabel("ic.data"); - B.condBr(isAccessor, miss, data); - B.startBlock(data); + const slots = load(host, "ptr", d, 32); + const address = B.tmp(); + B.line(`${address} = getelementptr inbounds ptr, ptr ${slots}, i64 ${slot}`); + return address; } /** The inline part of a load site, entered for an ordinary object `d`: @@ -137,7 +120,7 @@ function inlineLoad( B.br(miss); return; } - const shape = load(host, "ptr", d, 80); + const shape = load(host, "ptr", d, 80); // v.obj.layout const present = B.tmp(); B.line(`${present} = icmp ne ptr ${shape}, null`); let next = B.newLabel("ic.w"); @@ -160,9 +143,7 @@ function inlineLoad( B.condBr(own, ownLabel, protoLabel); const hit = (holder: string): void => { const slot = load(host, "i64", ic, base + 32); - const entry = entryAt(host, holder, slot); - dataEntry(host, entry, miss); - const value = load(host, "ptr", entry, 16); + const value = load(host, "ptr", slotAt(host, holder, slot), 0); const out = result(value); incoming.push(`[ ${out}, %${B.currentLabel()} ]`); B.br(join); @@ -194,7 +175,7 @@ function inlineLoad( /** The inline part of a store site that replaces an own data property, * entered for an ordinary object `d` outside an intact parse region: * tests the first `ways` cache entries; a hit calls `store` with the - * address of the entry's value word, which must end its block by + * address of the property's slot, which must end its block by * branching to the site's join. */ function inlineReplace( host: Host, @@ -209,7 +190,7 @@ function inlineReplace( B.br(miss); return; } - const shape = load(host, "ptr", d, 80); + const shape = load(host, "ptr", d, 80); // v.obj.layout const present = B.tmp(); B.line(`${present} = icmp ne ptr ${shape}, null`); let next = B.newLabel("ic.w"); @@ -230,21 +211,7 @@ function inlineReplace( B.condBr(ok, matched, next); B.startBlock(matched); const slot = load(host, "i64", ic, base + 32); - const entry = entryAt(host, d, slot); - const flags = load(host, "i8", entry, 40); - const writable = load(host, "i8", entry, 41); - const accessor = B.tmp(); - const data = B.tmp(); - const canWrite = B.tmp(); - const writes = B.tmp(); - B.line(`${accessor} = and i8 ${flags}, 1`); - B.line(`${data} = icmp eq i8 ${accessor}, 0`); - B.line(`${canWrite} = icmp ne i8 ${writable}, 0`); - B.line(`${writes} = and i1 ${data}, ${canWrite}`); - const hit = B.newLabel("ic.st"); - B.condBr(writes, hit, miss); - B.startBlock(hit); - store(gep(host, entry, 16)); + store(slotAt(host, d, slot)); } } diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index abacd5c8c..d940ae604 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -389,7 +389,7 @@ const RUNTIME_DEFINES: readonly { when: (opts: CcOptions) => boolean | undefined; }[] = [ { define: "SCR_TEXT_DECODER_LEGACY", when: (opts) => opts.textDecoderLegacy }, - // Checked objects carry the dynamic tier's shapes (scr_json.c's SCR_JS_TIER build); only + // scr_json.c's SCR_JS_TIER build exports the dynamic tier's cache primitives; only // programs with dynamic-tier code select it. { define: "SCR_JS_TIER", when: (opts) => opts.dynTier === true }, { define: "SCR_WARNING_REPLACE", when: (opts) => opts.warningReplace }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 82071fde2..0fb4ce815 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -1369,7 +1369,7 @@ function scanRuntimeFeatures( for (const t of nodes.types) type(t); } else if (keepGoing()) everyModuleNode(mod, { expr, stmt: keepGoing, type }); for (const { name, scan } of scans) if (scan.result()) features[name] = true; - // Inline-cache sites of dynamic-tier functions use the tier's shapes + // Inline-cache sites of dynamic-tier functions use the tier's caches // (scr_js_object.c, with the SCR_JS_TIER build of scr_json.c). if (!features.dynTier && mod.functions.some((fn) => fn.tier === "dynamic")) { const sites = new JsIcSites(mod); diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 16178e851..4ba2210fe 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -187,9 +187,9 @@ export const OPTIONAL_RUNTIME_SOURCES = [ export function variantsFor(source) { const variants = [{ id: "default", when: {}, defines: [] }]; - // Programs with dynamic-tier code keep the tier's hidden classes current - // in checked objects (two-tier design S4); every other program links the - // ordinary object, byte for byte. + // Programs with dynamic-tier code link the build that exports the + // primitives the tier's inline caches use (two-tier design S4); every + // other program links the ordinary object, byte for byte. if (source === "scr_json.c") { variants.push({ id: "dyn-tier", when: { dynTier: true }, defines: ["SCR_JS_TIER"] }); } diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index ebb274a23..c42a7b9e9 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -470,9 +470,16 @@ struct ScrDyn { struct { ScrSym *value; void (*release)(ScrSym *); ScrStr *(*render)(ScrSym *); ScrStr *(*description)(ScrSym *); } symbol; struct { size_t len; size_t cap; ScrDyn **items; struct ScrDyn *properties; bool sealed; bool frozen; unsigned char *presence; struct ScrDynRegion *region; } arr; /* owned; NULL presence means dense; region: see ScrDynRegion */ struct { - size_t len; - size_t cap; - ScrDynEntry *entries; + size_t len; /* own string-keyed properties, in both modes */ + size_t cap; /* capacity of entries (dictionary mode) or slots (layout mode) */ + /* Layout mode (`layout` non-NULL): slots[i] holds the value of the + * property layout->keys[i] names, a writable, enumerable, + * configurable data property. Dictionary mode (`layout` NULL): + * entries hold keys, values and attributes. See ScrLayout. */ + union { + ScrDynEntry *entries; + struct ScrDyn **slots; + }; /* Optional identity of the typed record that produced this ordinary * snapshot. Empty records and callback interfaces can retain their * source across repeated static-to-dynamic crossings. */ @@ -480,7 +487,7 @@ struct ScrDyn { * interfaces may request a fresh snapshot with the true arm. */ void *source_identity; ScrDyn *(*source_access)(void *, bool materialize); - struct ScrDynObjectIndex *index; /* optional wide-object lookup; owns no values */ + struct ScrDynObjectIndex *index; /* dictionary mode: optional wide-object lookup; owns no values */ /* On a runtime-built class prototype (e.g. url.Url.prototype) and * on a compiled class's prototype object: the constructor name * (static or immortal text) util.inspect prints for objects @@ -489,19 +496,13 @@ struct ScrDyn { * exits, and those clear it before release; reuse of a freed object * resets it (scr_dyn_alloc). */ const char *class_name; - union { - struct ScrDynRegion *region; /* see ScrDynRegion */ - /* While the node is pooled: the key bytes it keeps for reuse, - * counted in the pool's key budget (scr_dyn_obj_drop_keys). */ - size_t cached_key_bytes; - }; - /* The dynamic tier's hidden class (scr_js_object.c): the transition - * tree node naming this object's key sequence, or NULL while it has - * none. Only programs with dynamic-tier code read or maintain it: - * their runtime is built with SCR_JS_TIER, where every allocation and - * every change to the key set resets it. Other programs never look - * at it, so it may hold anything there. */ - struct ScrJsShape *shape; + struct ScrDynRegion *region; /* see ScrDynRegion */ + /* The object's layout in layout mode, NULL in dictionary mode. The + * dynamic tier's inline caches key on it. */ + const struct ScrLayout *layout; + /* Layout mode: bit i is the collector's snapshot of whether slot i's + * value is traced (ScrDynEntry.value_traced in dictionary mode). */ + size_t slots_traced; } obj; /* owned */ /* SCR_DYN_FUNC: the boxed closure (owned) + its call descriptor. `sig` * and `name` are static compiler-emitted literals (never freed); name @@ -580,17 +581,86 @@ _Static_assert(offsetof(ScrDyn, v.typed_ref.casts) == 16 + 8 * sizeof(void *), _Static_assert(sizeof(((ScrDyn *)0)->v.arr) <= sizeof(((ScrDyn *)0)->v.typed_ref) && sizeof(((ScrDyn *)0)->v.obj) <= sizeof(((ScrDyn *)0)->v.typed_ref), "dyn region links must not grow the node"); -/* Dynamic-tier inline caches read the shape word in place (64-bit). */ -_Static_assert(sizeof(void *) != 8 || offsetof(ScrDyn, v.obj.shape) == 80, "v.obj.shape offset"); +/* Dynamic-tier inline caches read the layout word and slots in place (64-bit). */ +_Static_assert(sizeof(void *) != 8 || offsetof(ScrDyn, v.obj.layout) == 80, "v.obj.layout offset"); +_Static_assert(sizeof(void *) != 8 || offsetof(ScrDyn, v.obj.slots) == 32, "v.obj.slots offset"); _Static_assert(sizeof(void *) != 8 || offsetof(ScrDyn, v.obj.region) == 72, "v.obj.region offset"); +/* ── layouts ─────────────────────────────────────────────────────────── + * An ordinary object (SCR_DYN_OBJ) is in one of two modes. + * + * Layout mode: v.obj.layout names the object's key sequence, a node of the + * thread's interned transition tree, and v.obj.slots holds one value per + * key (8 bytes per property). Every property of a layout-mode object is a + * writable, enumerable, configurable data property; objects of one key + * sequence share one layout, so a layout pointer proves which key sits at + * each slot and which keys the object lacks (the dynamic tier's inline + * caches key on it). New objects start in layout mode at the thread's + * empty layout. + * + * Dictionary mode: v.obj.layout is NULL and v.obj.entries holds keys, + * values and attributes (ScrDynEntry). An object moves there, once and for + * good, when it gets an accessor, a non-default attribute, a delete, more + * than SCR_LAYOUT_MAX_KEYS keys, when the thread's layout budgets are + * spent, when it is published to other threads, and whenever code that + * reads entries directly touches it (scr_dyn_obj_dict_entries): the conversion + * is unobservable, so code that has not been taught layouts stays correct. + * + * Layouts belong to one thread and live until its runtime state is torn + * down; they are never owned by a parse region. Each holds one reference + * to its key's bytes (scr_json.c's shared key storage). */ +#define SCR_LAYOUT_MAX_KEYS (8 * sizeof(size_t)) + +typedef struct ScrLayoutKey { + char *key; /* shared key bytes; one reference belongs to the layout that added it */ + uint32_t len; + uint32_t hash; /* low bits of scr_key_hash */ +} ScrLayoutKey; + +typedef struct ScrLayout { + const ScrLayoutKey *keys; /* keys[0..count); shared along a chain of layouts */ + uint32_t count; + uint32_t lookup_mask; /* capacity - 1 of `lookup`; 0 while it has none */ + uint8_t *lookup; /* lazily built hash index: slot + 1, 0 = empty */ + const struct ScrLayout *parent; + struct ScrLayout *last_child; /* the most recent transition from here */ + struct ScrLayoutKeys *store; /* the key array `keys` lives in */ + struct ScrLayout *all_next; + bool owns_store; +} ScrLayout; + +/* The index of `key` in layout `layout`, or UINT32_MAX. */ +uint32_t scr_layout_find(const ScrLayout *layout, const char *key, size_t key_len); +/* The layout that appends `key` (borrowed bytes) to `layout`, created on + * first use; NULL past SCR_LAYOUT_MAX_KEYS or the thread's budgets. */ +const ScrLayout *scr_layout_add(const ScrLayout *layout, const char *key, size_t key_len); +/* The thread's empty layout. */ +const ScrLayout *scr_layout_root(void); +/* Moves a layout-mode object to dictionary mode (no-op otherwise). */ +void scr_dyn_to_dictionary(ScrDyn *object); + +/* v.obj.entries of an object in dictionary mode, converting a layout-mode + * object first: code that reads or writes entries directly goes through + * this. */ +static inline __attribute__((always_inline)) ScrDynEntry *scr_dyn_obj_dict_entries(const ScrDyn *object) { + if (__builtin_expect(object->v.obj.layout != NULL, 0)) scr_dyn_to_dictionary((ScrDyn *)object); + return object->v.obj.entries; +} + +/* scr_dyn_obj_key_at and scr_dyn_obj_value_at out of line, for emitted + * code (borrowed results). */ +const char *scr_dyn_member_key(const ScrDyn *object, size_t i); +size_t scr_dyn_member_key_len(const ScrDyn *object, size_t i); +ScrDyn *scr_dyn_member_value(const ScrDyn *object, size_t i); + /* ── own properties of ordinary objects ────────────────────────────── - * Code outside the object core (scr_json.c and scr_js_object.c) reads and + * The accessors are always inlined, so a binary that never sees an + * ordinary object links no out-of-line copies. Code outside the object core (scr_json.c and scr_js_object.c) reads and * adjusts an SCR_DYN_OBJ's own string-keyed properties through these - * accessors, never through v.obj.entries, so the object's storage can - * change underneath (the single object model's generic objects). Index i - * runs over the properties in creation order, 0 <= i < scr_dyn_obj_count. - * Keys and values are borrowed: they stay valid until the object changes. */ + * accessors, never through v.obj.entries or v.obj.slots, so they work in + * both modes. Index i runs over the properties in creation order, + * 0 <= i < scr_dyn_obj_count. Keys and values are borrowed: they stay + * valid until the object changes. */ typedef struct { const char *key; size_t key_len; @@ -603,30 +673,44 @@ typedef struct { bool configurable; } ScrDynProp; -static inline size_t scr_dyn_obj_count(const ScrDyn *o) { +static inline __attribute__((always_inline)) size_t scr_dyn_obj_count(const ScrDyn *o) { return o->v.obj.len; } -static inline const char *scr_dyn_obj_key_at(const ScrDyn *o, size_t i, size_t *key_len) { +static inline __attribute__((always_inline)) const char *scr_dyn_obj_key_at(const ScrDyn *o, size_t i, size_t *key_len) { + const ScrLayout *layout = o->v.obj.layout; + if (layout) { + *key_len = layout->keys[i].len; + return layout->keys[i].key; + } *key_len = o->v.obj.entries[i].key_len; return o->v.obj.entries[i].key; } /* The value of data property i (borrowed); the undefined value for an * accessor property, whose functions scr_dyn_obj_prop_at answers. */ -static inline ScrDyn *scr_dyn_obj_value_at(const ScrDyn *o, size_t i) { - return o->v.obj.entries[i].value; +static inline __attribute__((always_inline)) ScrDyn *scr_dyn_obj_value_at(const ScrDyn *o, size_t i) { + return o->v.obj.layout ? o->v.obj.slots[i] : o->v.obj.entries[i].value; } -static inline ScrDynProp scr_dyn_obj_prop_at(const ScrDyn *o, size_t i) { +static inline __attribute__((always_inline)) ScrDynProp scr_dyn_obj_prop_at(const ScrDyn *o, size_t i) { + const ScrLayout *layout = o->v.obj.layout; + if (layout) + return (ScrDynProp){layout->keys[i].key, layout->keys[i].len, o->v.obj.slots[i], NULL, NULL, + false, true, true, true}; const ScrDynEntry *e = &o->v.obj.entries[i]; return (ScrDynProp){e->key, e->key_len, e->value, e->getter, e->setter, e->accessor, e->writable, e->enumerable, e->configurable}; } -/* The index of own property `key`, or SIZE_MAX. A linear scan: for code - * that looks up options and headers, not for hot property reads. */ +/* The index of own property `key`, or SIZE_MAX. A linear scan in + * dictionary mode: for code that looks up options and headers, not for hot + * property reads. */ static inline size_t scr_dyn_obj_index_of(const ScrDyn *o, const char *key, size_t key_len) { + if (o->v.obj.layout) { + uint32_t i = scr_layout_find(o->v.obj.layout, key, key_len); + return i == UINT32_MAX ? SIZE_MAX : i; + } for (size_t i = 0; i < o->v.obj.len; i++) { const ScrDynEntry *e = &o->v.obj.entries[i]; if (e->key_len == key_len && memcmp(e->key, key, key_len) == 0) return i; @@ -639,7 +723,8 @@ static inline size_t scr_dyn_obj_index_of(const ScrDyn *o, const char *key, size * members this way right after adding them. */ static inline void scr_dyn_obj_set_attrs_at(ScrDyn *o, size_t i, bool writable, bool enumerable, bool configurable) { - ScrDynEntry *e = &o->v.obj.entries[i]; + if (o->v.obj.layout && writable && enumerable && configurable) return; + ScrDynEntry *e = &scr_dyn_obj_dict_entries(o)[i]; if (!e->accessor) e->writable = writable; e->enumerable = enumerable; e->configurable = configurable; @@ -821,6 +906,13 @@ static inline bool scr_dyn_own_symbol_prop(const ScrDyn *object, const ScrDyn *k } /* Copies own property i of `source` to `target` (scr_dyn_copy_entry). */ static inline void scr_dyn_copy_prop_at(ScrDyn *target, const ScrDyn *source, size_t i) { + if (source->v.obj.layout) { + ScrDynProp prop = scr_dyn_obj_prop_at(source, i); + ScrDynEntry entry = {(char *)prop.key, prop.key_len, prop.value, NULL, NULL, + false, false, false, true, true, true}; + scr_dyn_copy_entry(target, source, &entry); + return; + } scr_dyn_copy_entry(target, source, &source->v.obj.entries[i]); } /* The private storage name of a symbol key (+1). With it, @@ -2054,7 +2146,7 @@ void scr_dyn_trace_v(void *d, ScrTraceVisit visit, void *ctx); long scr_dyn_live_count(void); #endif -/* Checked-object primitives the dynamic tier's shapes and caches build on +/* Checked-object primitives the dynamic tier's inline caches build on * (scr_js_object.c). They exist only in the SCR_JS_TIER build of * scr_json.c, which programs with dynamic-tier code link. */ /* The builtin Object.prototype node, or NULL before its first use. */ @@ -2062,12 +2154,9 @@ const ScrDyn *scr_dyn_tier_object_prototype(void); /* A read site's answer for a native array's length or a native iterator's * next (scr_js_get_probe_native): +1, or NULL. */ ScrDyn *scr_dyn_tier_unshaped_get(const ScrDyn *receiver, const ScrStr *key); -/* Shared key bytes for `key` (+1, NUL-terminated), and their release. */ -char *scr_dyn_tier_key_copy(const char *key, size_t key_len); -void scr_dyn_tier_key_release(char *key, size_t key_len); -/* Appends an own data property (writable, enumerable, configurable) that - * `obj` does not have, sharing `key`'s bytes and taking ownership of - * `value`, exactly as an ordinary insertion would; leaves the shape reset. */ -void scr_dyn_tier_append(ScrDyn *obj, char *key, size_t key_len, ScrDyn *value); +/* Appends to a layout-mode object the property that `next`, a child of + * its layout, adds, taking ownership of `value`, exactly as an ordinary + * insertion would. */ +void scr_dyn_tier_append(ScrDyn *obj, const ScrLayout *next, ScrDyn *value); #endif /* SCR_DYN_H */ diff --git a/packages/runtime/src/scr_js.h b/packages/runtime/src/scr_js.h index 5abab2053..e3c37560d 100644 --- a/packages/runtime/src/scr_js.h +++ b/packages/runtime/src/scr_js.h @@ -180,45 +180,37 @@ ScrJsVal scr_js_increment(ScrJsVal value, bool increment); double scr_js_to_number(ScrJsVal value); bool scr_js_truthy(ScrJsVal value); -/* ── shapes and inline caches (scr_js_object.c) ───────────────────────── - * A checked object's hidden class (ScrDyn.v.obj.shape) names its key - * sequence: a node of the thread's transition tree, whose depth is the - * entry count and whose last key sits at entry index depth - 1. Shapes are - * per thread and live until the thread's runtime state is torn down; a - * thread's shape count and the key bytes they hold are bounded, and an - * object whose key sequence would need a shape past either bound stays - * unshaped, as do immortal objects, which threads share. The tier - * computes an object's shape on a cache miss; the SCR_JS_TIER build of - * scr_json.c resets it on every change to the key set. - * Attributes stay in the entries and the prototype in the node: cache hits - * test both live, so a shape only ever proves which key sits at which entry - * index and which keys an object lacks. +/* ── inline caches (scr_js_object.c) ─────────────────────────────────── + * Caches key on object layouts (scr_dyn.h): a layout-mode object's layout + * names its key sequence, and all its properties are writable data + * properties, so a layout proves which key sits at which slot and which + * keys the object lacks. Dictionary-mode objects (layout NULL), including + * immortal objects, which threads share, never hit. The prototype stays in + * the node: cache hits test it live. * * Each property site of compiled dynamic-tier code owns one zeroed, * thread-local ScrJsIc with static storage: the reset that frees a - * thread's shapes (at thread exit, and when a library session ends) clears + * thread's layouts (at thread exit, and when a library session ends) clears * every record that holds entries. The code at the site is fixed: it tests * entry 0 inline and calls a probe for everything else. Probes never run * user code: they answer from plain data properties or return a miss * (NULL / false) with nothing done, and the site then runs its generic * path. * - * load entry: shape = receiver shape; target = NULL for an own property - * (slot indexes the receiver's entries), else the shape of - * `holder`, the receiver's prototype, whose entry `slot` holds it. - * store entry: shape = receiver shape before the store; target == shape - * replaces entry `slot`; any other target adds the key as entry - * `slot` and moves the receiver to `target`. An add requires the - * receiver's prototype to be `holder` (NULL or an object whose - * shape is holder_shape and that lacks the key). - * keyed entry: slot only; a hit compares the key bytes stored there. */ -typedef struct ScrJsShape ScrJsShape; - + * load entry: layout = receiver layout; target = NULL for an own + * property (slot indexes the receiver's slots), else the layout of + * `holder`, the receiver's prototype, whose slot `slot` holds it. + * store entry: layout = receiver layout before the store; target == + * layout replaces slot `slot`; any other target adds the key as + * slot `slot` and moves the receiver to `target`. An add requires + * the receiver's prototype to be `holder` (NULL or an object whose + * layout is holder_layout and that lacks the key). + * keyed entry: slot only; a hit compares the key stored there. */ typedef struct ScrJsIcEntry { - const ScrJsShape *shape; - const ScrJsShape *target; + const ScrLayout *layout; + const ScrLayout *target; const ScrDyn *holder; - const ScrJsShape *holder_shape; + const ScrLayout *holder_layout; size_t slot; } ScrJsIcEntry; diff --git a/packages/runtime/src/scr_js_object.c b/packages/runtime/src/scr_js_object.c index 4cee9e7e8..4d94a4e7e 100644 --- a/packages/runtime/src/scr_js_object.c +++ b/packages/runtime/src/scr_js_object.c @@ -1,19 +1,15 @@ -/* scriptc runtime — the dynamic tier's hidden classes and inline caches - * (scr_js.h; two-tier design stage S4). +/* scriptc runtime — the dynamic tier's inline caches (scr_js.h; two-tier + * design stage S4). * - * Shapes are attached to the existing checked objects (ScrDyn OBJ nodes) - * rather than replacing their entry storage: static code and the shared - * runtime read those entries in place, so a second object representation - * would have to be understood or converted at every boundary. A shape is a - * node of the thread's transition tree over key sequences; an object's - * shape proves which key sits at each entry index and which keys the - * object lacks. Everything else a cache hit depends on is tested live: the - * node kind, the accessor and writable bits of the entry it uses, and the - * prototype pointer. + * Caches key on object layouts (scr_dyn.h): a layout-mode object's layout + * proves which key sits at each slot and which keys the object lacks, and + * every property of such an object is a writable data property, so a hit + * tests the layout word and, for prototype hits, the prototype pointer. + * Dictionary-mode objects (layout NULL) never hit and take the generic + * paths. * * Linked only into programs with dynamic-tier code (the dynTier runtime - * feature), together with the SCR_JS_TIER build of scr_json.c, which - * resets an object's shape on every change to its key set. */ + * feature). */ #include "scr_js.h" #include "scr_key.h" @@ -24,61 +20,26 @@ * backend/llvm/js-ic.ts). */ #if SCR_JS_IMMEDIATES _Static_assert(offsetof(ScrDyn, kind) == 8, "kind offset"); -_Static_assert(offsetof(ScrDyn, v.obj.len) == 16 && offsetof(ScrDyn, v.obj.entries) == 32, - "object entry offsets"); +_Static_assert(offsetof(ScrDyn, v.obj.len) == 16 && offsetof(ScrDyn, v.obj.slots) == 32 && + offsetof(ScrDyn, v.obj.layout) == 80, "object slot offsets"); _Static_assert(offsetof(ScrDyn, v.arr.len) == 16 && offsetof(ScrDyn, v.arr.items) == 32 && offsetof(ScrDyn, v.arr.presence) == 56, "array element offsets"); _Static_assert(offsetof(ScrDyn, prototype) == 96, "prototype offset"); -_Static_assert(sizeof(ScrDynEntry) == 48 && offsetof(ScrDynEntry, value) == 16 && - offsetof(ScrDynEntry, writable) == 41, "entry layout"); _Static_assert(sizeof(ScrJsIcEntry) == 40 && sizeof(ScrJsIc) == 168, "cache layout"); #endif -/* Objects with more entries stay unshaped (dictionary mode): their reads - * take the generic path and its hash index. */ -#define SCR_JS_SHAPE_MAX 64 - -/* Shapes live until the thread exits or a library session ends, so a thread - * creates at most this many, holding at most this many key bytes. Once - * either budget is spent, objects whose key sequence has no shape yet stay - * unshaped; shapes that exist keep serving. */ -#define SCR_JS_SHAPE_BUDGET ((size_t)1 << 16) -#define SCR_JS_SHAPE_KEY_BYTES_BUDGET ((size_t)4 << 20) - -struct ScrJsShape { - const ScrJsShape *parent; - char *key; /* one reference to shared key bytes (scr_json.c) */ - size_t key_len; - uint64_t hash; - uint32_t count; /* entries: the depth in the tree */ - ScrJsShape *all_next; -}; - -/* The root is the empty key sequence. Transitions live in one open - * addressing table per thread keyed by (parent, key). */ -static SCR_TL ScrJsShape scr_js_root; -static SCR_TL ScrJsShape *scr_js_all_shapes; -static SCR_TL ScrJsShape **scr_js_transitions; -static SCR_TL size_t scr_js_transition_mask; /* capacity - 1; 0 when absent */ -static SCR_TL size_t scr_js_transition_count; -static SCR_TL size_t scr_js_shape_key_bytes; -static SCR_TL bool scr_js_shapes_registered; - -/* The cache records that hold entries. Entries name shapes and prototype - * objects, so the reset that frees the shapes also clears these records: a - * library's next session runs the same sites with the same records. */ +/* The cache records that hold entries. Entries name layouts and prototype + * objects, so the reset of a thread's runtime state (which frees its + * layouts) also clears these records: a library's next session runs the + * same sites with the same records. */ static SCR_TL ScrJsIc **scr_js_filled_ics; static SCR_TL size_t scr_js_filled_count; static SCR_TL size_t scr_js_filled_capacity; - -/* The shape word of an object whose key sequence has no shape within the - * budgets. No cache entry names it, so its accesses take the probes, - * which answer with a miss until the key set changes. */ -static ScrJsShape scr_js_unshaped; +static SCR_TL bool scr_js_ics_registered; /* The second-level cache of load sites whose own record is full: one entry - * per (receiver shape, key bytes) slot, filled by the probe and checked like - * a site entry. It names shapes, so the shape reset clears it. */ + * per (receiver layout, key bytes) slot, filled by the probe and checked + * like a site entry. It names layouts, so the reset clears it. */ #define SCR_JS_SHARED_LOADS 2048 typedef struct ScrJsSharedLoad { const char *key; @@ -86,7 +47,7 @@ typedef struct ScrJsSharedLoad { } ScrJsSharedLoad; static SCR_TL ScrJsSharedLoad *scr_js_shared_loads; -static void scr_js_shapes_reset(void) { +static void scr_js_ics_reset(void) { for (size_t i = 0; i < scr_js_filled_count; i++) memset(scr_js_filled_ics[i], 0, sizeof *scr_js_filled_ics[i]); free(scr_js_shared_loads); @@ -95,130 +56,33 @@ static void scr_js_shapes_reset(void) { scr_js_filled_ics = NULL; scr_js_filled_count = 0; scr_js_filled_capacity = 0; - while (scr_js_all_shapes) { - ScrJsShape *shape = scr_js_all_shapes; - scr_js_all_shapes = shape->all_next; - scr_dyn_tier_key_release(shape->key, shape->key_len); - free(shape); - } - free(scr_js_transitions); - scr_js_transitions = NULL; - scr_js_transition_mask = 0; - scr_js_transition_count = 0; - scr_js_shape_key_bytes = 0; - scr_js_shapes_registered = false; + scr_js_ics_registered = false; } -static size_t scr_js_transition_slot(const ScrJsShape *parent, uint64_t hash) { - return (size_t)scr_key_mix(hash ^ (uint64_t)(uintptr_t)parent); -} - -static void scr_js_transition_insert(ScrJsShape *child) { - size_t i = scr_js_transition_slot(child->parent, child->hash) & scr_js_transition_mask; - while (scr_js_transitions[i]) i = (i + 1) & scr_js_transition_mask; - scr_js_transitions[i] = child; -} - -static void scr_js_transitions_grow(void) { - size_t old_cap = scr_js_transition_mask ? scr_js_transition_mask + 1 : 0; - size_t cap = old_cap ? old_cap * 2 : 256; - ScrJsShape **old = scr_js_transitions; - scr_js_transitions = calloc(cap, sizeof *scr_js_transitions); - if (!scr_js_transitions) scr_trap("scriptc: out of memory\n"); - scr_js_transition_mask = cap - 1; - for (size_t i = 0; i < old_cap; i++) - if (old[i]) scr_js_transition_insert(old[i]); - free(old); -} - -/* The child of `parent` for appending `key` (borrowed bytes), created on - * first use; NULL when creating it would exceed a budget. */ -static const ScrJsShape *scr_js_transition(const ScrJsShape *parent, const char *key, size_t key_len) { - uint64_t hash = scr_key_hash(key, key_len); - if (scr_js_transition_mask) { - size_t i = scr_js_transition_slot(parent, hash) & scr_js_transition_mask; - for (ScrJsShape *child; (child = scr_js_transitions[i]); i = (i + 1) & scr_js_transition_mask) { - if (child->parent == parent && child->hash == hash && child->key_len == key_len && - scr_key_equal(child->key, key, key_len)) - return child; - } - } - if (scr_js_transition_count >= SCR_JS_SHAPE_BUDGET || - key_len > SCR_JS_SHAPE_KEY_BYTES_BUDGET - scr_js_shape_key_bytes) - return NULL; - if ((scr_js_transition_count + 1) * 2 > (scr_js_transition_mask ? scr_js_transition_mask + 1 : 0)) - scr_js_transitions_grow(); - ScrJsShape *child = malloc(sizeof *child); - if (!child) scr_trap("scriptc: out of memory\n"); - *child = (ScrJsShape){parent, scr_dyn_tier_key_copy(key, key_len), key_len, hash, parent->count + 1, - scr_js_all_shapes}; - scr_js_all_shapes = child; - if (!scr_js_shapes_registered) { - scr_js_shapes_registered = true; - scr_atexit(scr_js_shapes_reset); - } - scr_js_transition_insert(child); - scr_js_transition_count++; - scr_js_shape_key_bytes += key_len; - return child; -} - -/* An object's shape, computed from its entries when it has none; NULL for - * objects that stay unshaped (other kinds, immortal objects, the builtin - * Object.prototype, dictionary-sized objects, key sequences past the - * budgets). Immortal objects (rc == SIZE_MAX: published graphs, which - * threads share by pointer) are never shaped: shapes belong to one thread - * and die with it, and no thread writes an immortal object's header. */ -#ifdef SCR_JS_VERIFY -/* Debugging aid (build the runtime with -DSCR_JS_VERIFY and compile with - * SCRIPTC_JS_IC_WAYS=0 so every access probes): traps when an object's - * shape no longer names its key sequence, i.e. when some mutation of the - * key set missed its SCR_JS_SHAPE_RESET. */ -static void scr_js_verify(const ScrDyn *object, const ScrJsShape *shape) { - size_t len = object->v.obj.len; - if (shape->count != len) scr_trap("scriptc: stale shape (entry count)\n"); - for (const ScrJsShape *s = shape; s->parent; s = s->parent) { - const ScrDynEntry *entry = &object->v.obj.entries[s->count - 1]; - if (entry->key_len != s->key_len || !scr_key_equal(entry->key, s->key, s->key_len)) - scr_trap("scriptc: stale shape (key)\n"); - } -} -#endif - -static const ScrJsShape *scr_js_shape_of(ScrDyn *object) { +/* An object's layout, or NULL for objects caches leave alone: other kinds, + * dictionary-mode objects (immortal objects, which threads share, are + * always in dictionary mode), and the builtin Object.prototype. */ +static inline const ScrLayout *scr_js_layout_of(const ScrDyn *object) { if (object->kind != SCR_DYN_OBJ || object->rc == SIZE_MAX) return NULL; - const ScrJsShape *shape = object->v.obj.shape; - if (shape == &scr_js_unshaped) return NULL; -#ifdef SCR_JS_VERIFY - if (shape) scr_js_verify(object, shape); -#endif - if (shape) return shape; - size_t len = object->v.obj.len; - if (len > SCR_JS_SHAPE_MAX || object == scr_dyn_tier_object_prototype()) return NULL; - shape = &scr_js_root; - for (size_t i = 0; i < len; i++) { - ScrDynEntry *entry = &object->v.obj.entries[i]; - shape = scr_js_transition(shape, entry->key, entry->key_len); - if (!shape) { - object->v.obj.shape = &scr_js_unshaped; - return NULL; - } - } - object->v.obj.shape = (ScrJsShape *)shape; - return shape; + const ScrLayout *layout = object->v.obj.layout; + if (!layout || object == scr_dyn_tier_object_prototype()) return NULL; + return layout; } -/* The entry index of `key` in an object of shape `shape`, or SIZE_MAX. */ -static size_t scr_js_shape_find(const ScrJsShape *shape, const char *key, size_t key_len) { - for (; shape->parent; shape = shape->parent) - if (shape->key_len == key_len && scr_key_equal(shape->key, key, key_len)) return shape->count - 1; - return SIZE_MAX; +/* The slot of `key` in an object of layout `layout`, or SIZE_MAX. */ +static size_t scr_js_layout_find(const ScrLayout *layout, const char *key, size_t key_len) { + uint32_t slot = scr_layout_find(layout, key, key_len); + return slot == UINT32_MAX ? SIZE_MAX : slot; } /* Fills a cache entry: `stale` (a matching entry whose hit failed) when * there is one, else a free or replaceable one. */ static void scr_js_ic_add(ScrJsIc *ic, uint32_t stale, ScrJsIcEntry entry) { if (ic->count == 0) { + if (!scr_js_ics_registered) { + scr_js_ics_registered = true; + scr_atexit(scr_js_ics_reset); + } if (scr_js_filled_count == scr_js_filled_capacity) { size_t capacity = scr_js_filled_capacity ? scr_js_filled_capacity * 2 : 64; ScrJsIc **grown = realloc(scr_js_filled_ics, capacity * sizeof *grown); @@ -243,26 +107,25 @@ static void scr_js_ic_add(ScrJsIc *ic, uint32_t stale, ScrJsIcEntry entry) { /* ── loads ────────────────────────────────────────────────────────────── */ -/* The cached answer of an entry whose receiver shape matched, or NULL. */ +/* The cached answer of an entry whose receiver layout matched, or NULL. */ static ScrDyn *scr_js_load_hit(ScrDyn *receiver, const ScrJsIcEntry *entry) { const ScrDyn *holder = receiver; if (entry->target) { holder = entry->holder; if (receiver->prototype != holder || holder->kind != SCR_DYN_OBJ || - holder->v.obj.shape != entry->target) + holder->v.obj.layout != entry->target) return NULL; } - const ScrDynEntry *found = &holder->v.obj.entries[entry->slot]; - return found->accessor ? NULL : scr_dyn_retain(found->value); + return scr_dyn_retain(holder->v.obj.slots[entry->slot]); } /* An absent entry: its target is this tag and its holder is a placeholder * that is never a prototype, so the inline test of entry 0 in compiled code * (a prototype hit needs the receiver's prototype to be the holder) always - * sends such a receiver to the probe. Its holder_shape and slot name the one - * prototype the read walked past (shape and object), or NULL and 0 when the - * receiver had no prototype. */ -static const ScrJsShape scr_js_absent; + * sends such a receiver to the probe. Its holder_layout and slot name the + * one prototype the read walked past (layout and object), or NULL and 0 when + * the receiver had no prototype. */ +static const ScrLayout scr_js_absent; static const ScrDyn scr_js_absent_holder; /* Keys whose read on a plain object is not just its [[Get]]: the members of @@ -288,7 +151,7 @@ static bool scr_js_absent_cacheable(const ScrStr *key) { return true; } -static ScrJsSharedLoad *scr_js_shared_load(const ScrJsShape *shape, const ScrStr *key) { +static ScrJsSharedLoad *scr_js_shared_load(const ScrLayout *shape, const ScrStr *key) { if (!scr_js_shared_loads) { scr_js_shared_loads = calloc(SCR_JS_SHARED_LOADS, sizeof *scr_js_shared_loads); if (!scr_js_shared_loads) scr_trap("scriptc: out of memory\n"); @@ -305,7 +168,7 @@ static bool scr_js_absent_hit(const ScrDyn *receiver, const ScrJsIcEntry *entry) const ScrDyn *walked = (const ScrDyn *)entry->slot; if (!walked) return prototype == NULL; return prototype == walked && walked->kind == SCR_DYN_OBJ && - walked->v.obj.shape == entry->holder_shape && walked->prototype == NULL; + walked->v.obj.layout == entry->holder_layout && walked->prototype == NULL; } /* The answer of a matching entry for `receiver`, or NULL when the entry no @@ -344,11 +207,11 @@ static void scr_js_load_fill(ScrJsIc *ic, uint32_t stale, ScrJsSharedLoad *share * methods to the slow path, which names the missing function. */ static __attribute__((noinline)) ScrDyn *scr_js_probe_load(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic, bool absent) { - const ScrJsShape *shape = scr_js_shape_of(receiver); + const ScrLayout *shape = scr_js_layout_of(receiver); if (!shape) return NULL; uint32_t stale = UINT32_MAX; for (uint32_t i = 0; i < ic->count; i++) { - if (ic->entry[i].shape != shape) continue; + if (ic->entry[i].layout != shape) continue; ScrDyn *value = scr_js_entry_answer(receiver, &ic->entry[i]); if (value) return value; if (stale == UINT32_MAX) stale = i; @@ -356,18 +219,16 @@ static __attribute__((noinline)) ScrDyn *scr_js_probe_load(ScrDyn *receiver, con ScrJsSharedLoad *shared = NULL; if (ic->count == SCR_JS_IC_WAYS) { shared = scr_js_shared_load(shape, key); - if (shared->key == key->data && shared->entry.shape == shape && + if (shared->key == key->data && shared->entry.layout == shape && (absent || shared->entry.target != &scr_js_absent)) { ScrDyn *value = scr_js_entry_answer(receiver, &shared->entry); if (value) return value; } } - size_t slot = scr_js_shape_find(shape, key->data, key->len); + size_t slot = scr_js_layout_find(shape, key->data, key->len); if (slot != SIZE_MAX) { - const ScrDynEntry *found = &receiver->v.obj.entries[slot]; - if (found->accessor) return NULL; scr_js_load_fill(ic, stale, shared, key, (ScrJsIcEntry){shape, NULL, NULL, NULL, slot}); - return scr_dyn_retain(found->value); + return scr_dyn_retain(receiver->v.obj.slots[slot]); } absent = absent && scr_js_absent_cacheable(key); /* One prototype link: an ordinary object other than the builtin @@ -379,9 +240,9 @@ static __attribute__((noinline)) ScrDyn *scr_js_probe_load(ScrDyn *receiver, con (ScrJsIcEntry){shape, &scr_js_absent, &scr_js_absent_holder, NULL, 0}); return scr_dyn_retain(scr_dyn_undefined()); } - const ScrJsShape *holder_shape = scr_js_shape_of(holder); + const ScrLayout *holder_shape = scr_js_layout_of(holder); if (!holder_shape) return NULL; - slot = scr_js_shape_find(holder_shape, key->data, key->len); + slot = scr_js_layout_find(holder_shape, key->data, key->len); if (slot == SIZE_MAX) { if (!absent || holder->prototype != NULL) return NULL; scr_js_load_fill(ic, stale, shared, key, @@ -389,13 +250,11 @@ static __attribute__((noinline)) ScrDyn *scr_js_probe_load(ScrDyn *receiver, con (size_t)holder}); return scr_dyn_retain(scr_dyn_undefined()); } - const ScrDynEntry *found = &holder->v.obj.entries[slot]; - if (found->accessor) return NULL; scr_js_load_fill(ic, stale, shared, key, (ScrJsIcEntry){shape, holder_shape, holder, NULL, slot}); - return scr_dyn_retain(found->value); + return scr_dyn_retain(holder->v.obj.slots[slot]); } -/* Receivers other than ordinary objects have no shape: they miss before +/* Receivers other than ordinary objects have no layout: they miss before * the probe sets up its frame. */ ScrDyn *scr_js_load_probe(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic) { if (receiver->kind != SCR_DYN_OBJ) return NULL; @@ -427,62 +286,56 @@ static bool scr_js_error_synced_key(const ScrStr *key) { } static bool scr_js_replace(ScrDyn *receiver, size_t slot, ScrDyn *value) { - ScrDynEntry *found = &receiver->v.obj.entries[slot]; - if (found->accessor || !found->writable || found->fenced) return false; scr_dyn_region_store(receiver, value); - ScrDyn *old = found->value; - found->value = scr_dyn_retain(value); + ScrDyn **found = &receiver->v.obj.slots[slot]; + ScrDyn *old = *found; + *found = scr_dyn_retain(value); scr_dyn_release(old); return true; } /* Whether an add may skip the generic path's prototype-chain walk: the * chain is empty, or one ordinary object that lacks the key and ends it. */ -static bool scr_js_add_chain(const ScrDyn *prototype, const ScrJsShape *prototype_shape, +static bool scr_js_add_chain(const ScrDyn *prototype, const ScrLayout *prototype_shape, const ScrStr *key) { if (!prototype) return true; return prototype_shape && !prototype->prototype && - scr_js_shape_find(prototype_shape, key->data, key->len) == SIZE_MAX; + scr_js_layout_find(prototype_shape, key->data, key->len) == SIZE_MAX; } -static bool scr_js_append_shaped(ScrDyn *receiver, const ScrJsShape *target, ScrDyn *value) { +static bool scr_js_append_shaped(ScrDyn *receiver, const ScrLayout *target, ScrDyn *value) { if (receiver->non_extensible) return false; - scr_dyn_tier_append(receiver, target->key, target->key_len, scr_dyn_retain(value)); - receiver->v.obj.shape = (ScrJsShape *)target; + scr_dyn_tier_append(receiver, target, scr_dyn_retain(value)); return true; } bool scr_js_store_probe(ScrDyn *receiver, const ScrStr *key, ScrDyn *value, ScrJsIc *ic) { - const ScrJsShape *shape = scr_js_shape_of(receiver); + const ScrLayout *shape = scr_js_layout_of(receiver); if (!shape || scr_js_error_synced_key(key)) return false; uint32_t stale = UINT32_MAX; for (uint32_t i = 0; i < ic->count; i++) { const ScrJsIcEntry *entry = &ic->entry[i]; - if (entry->shape != shape) continue; + if (entry->layout != shape) continue; if (entry->target == shape) { if (scr_js_replace(receiver, entry->slot, value)) return true; } else if (receiver->prototype == entry->holder && (!entry->holder || (entry->holder->kind == SCR_DYN_OBJ && - entry->holder->v.obj.shape == entry->holder_shape && + entry->holder->v.obj.layout == entry->holder_layout && !entry->holder->prototype))) { if (scr_js_append_shaped(receiver, entry->target, value)) return true; } stale = i; break; } - size_t slot = scr_js_shape_find(shape, key->data, key->len); + size_t slot = scr_js_layout_find(shape, key->data, key->len); if (slot != SIZE_MAX) { - const ScrDynEntry *found = &receiver->v.obj.entries[slot]; - if (found->accessor || !found->writable || found->fenced) return false; scr_js_ic_add(ic, stale, (ScrJsIcEntry){shape, shape, NULL, NULL, slot}); return scr_js_replace(receiver, slot, value); } ScrDyn *prototype = receiver->prototype; - const ScrJsShape *prototype_shape = prototype ? scr_js_shape_of(prototype) : NULL; - if (!scr_js_add_chain(prototype, prototype_shape, key) || receiver->non_extensible || - shape->count >= SCR_JS_SHAPE_MAX) - return false; - const ScrJsShape *target = scr_js_transition(shape, key->data, key->len); + const ScrLayout *prototype_shape = prototype ? scr_js_layout_of(prototype) : NULL; + if (!scr_js_add_chain(prototype, prototype_shape, key) || receiver->non_extensible) return false; + const ScrLayout *target = scr_layout_add(shape, key->data, key->len); if (!target) return false; scr_js_ic_add(ic, stale, (ScrJsIcEntry){shape, target, prototype, prototype_shape, target->count - 1}); return scr_js_append_shaped(receiver, target, value); @@ -505,20 +358,18 @@ ScrDyn *scr_js_keyed_probe(ScrDyn *receiver, const ScrDyn *key, ScrJsIc *ic) { if (key->kind == SCR_DYN_NUM) return scr_js_index_probe(receiver, key->v.num); if (key->kind != SCR_DYN_STR || receiver->kind != SCR_DYN_OBJ) return NULL; const ScrStr *name = key->v.str; + const ScrLayout *shape = scr_js_layout_of(receiver); + if (!shape) return NULL; size_t len = receiver->v.obj.len; for (uint32_t i = 0; i < ic->count; i++) { size_t slot = ic->entry[i].slot; if (slot >= len) continue; - const ScrDynEntry *found = &receiver->v.obj.entries[slot]; - if (found->key_len == name->len && scr_key_equal(found->key, name->data, name->len)) - return found->accessor ? NULL : scr_dyn_retain(found->value); + const ScrLayoutKey *found = &shape->keys[slot]; + if (found->len == name->len && scr_key_equal(found->key, name->data, name->len)) + return scr_dyn_retain(receiver->v.obj.slots[slot]); } - const ScrJsShape *shape = scr_js_shape_of(receiver); - if (!shape) return NULL; - size_t slot = scr_js_shape_find(shape, name->data, name->len); + size_t slot = scr_js_layout_find(shape, name->data, name->len); if (slot == SIZE_MAX) return NULL; - const ScrDynEntry *found = &receiver->v.obj.entries[slot]; - if (found->accessor) return NULL; scr_js_ic_add(ic, UINT32_MAX, (ScrJsIcEntry){NULL, NULL, NULL, NULL, slot}); - return scr_dyn_retain(found->value); + return scr_dyn_retain(receiver->v.obj.slots[slot]); } diff --git a/packages/runtime/src/scr_js_object.test.c b/packages/runtime/src/scr_js_object.test.c index 06b804759..02368ca74 100644 --- a/packages/runtime/src/scr_js_object.test.c +++ b/packages/runtime/src/scr_js_object.test.c @@ -5,7 +5,7 @@ #include #include -/* Mirrors scr_js_object.c's budgets. */ +/* Mirrors scr_json.c's layout budgets. */ #define SHAPE_BUDGET ((size_t)1 << 16) #define KEY_BYTES_BUDGET ((size_t)4 << 20) @@ -63,7 +63,8 @@ static void budget_bounds_shapes(size_t key_len, size_t budget) { scr_str_release(key); scr_dyn_release(object); } - assert(shaped == budget); + /* Objects the earlier checks built hold a few layouts already. */ + assert(shaped <= budget && shaped + 64 > budget); /* A key sequence shaped before the budget ran out. */ ScrStr *first = key_of(bytes, key_len, 0); @@ -73,7 +74,7 @@ static void budget_bounds_shapes(size_t key_len, size_t budget) { /* A store probe that would add a new shape leaves the add to the generic * store; one that replaces through an existing shape still runs. */ memset(&store_ic, 0, sizeof store_ic); - ScrStr *added = scr_str_new("added", 5); + ScrStr *added = key_of(bytes, key_len, budget + 1000); ScrDyn *value = scr_dyn_new_num(8); assert(!scr_js_store_probe(object, added, value, &store_ic)); assert(object->v.obj.len == 1); @@ -94,11 +95,11 @@ static void immortal_objects_stay_unshaped(void) { ScrDyn *object = object_with(key, 3); object->rc = SIZE_MAX; assert(!probe_reads(object, key, 3)); - assert(object->v.obj.shape == NULL); + assert(object->v.obj.len == 1); memset(&store_ic, 0, sizeof store_ic); ScrDyn *value = scr_dyn_new_num(4); assert(!scr_js_store_probe(object, key, value, &store_ic)); - assert(object->v.obj.shape == NULL); + assert(object->v.obj.len == 1); scr_dyn_release(value); scr_str_release(key); } diff --git a/packages/runtime/src/scr_js_object.test.ts b/packages/runtime/src/scr_js_object.test.ts index 1fb90cdaa..49d24f65f 100644 --- a/packages/runtime/src/scr_js_object.test.ts +++ b/packages/runtime/src/scr_js_object.test.ts @@ -7,7 +7,7 @@ import { expect, test } from "vitest"; const exec = promisify(execFile); -test("dynamic-tier shapes stay within the shape and key-byte budgets, and absent reads are cached", async () => { +test("layouts stay within the layout and key-byte budgets, and dynamic-tier absent reads are cached", async () => { const scratch = await mkdtemp(join(tmpdir(), "scriptc-js-object-")); const src = import.meta.dirname; const binary = join(scratch, "js-object"); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index eb6f931e0..1f5491e1e 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -24,16 +24,6 @@ #include "scr_runtime.h" #include "scr_key.h" -/* Programs with dynamic-tier code link the SCR_JS_TIER build of this unit: - * its object nodes carry the tier's hidden class (ScrDyn.v.obj.shape), and - * every allocation and change to an object's key set resets it so the - * tier recomputes it on its next cache miss. Other programs link the - * ordinary build, where this expands to nothing. */ -#ifdef SCR_JS_TIER -#define SCR_JS_SHAPE_RESET(d) ((d)->v.obj.shape = NULL) -#else -#define SCR_JS_SHAPE_RESET(d) ((void)0) -#endif static SCR_TL ScrDyn *scr_builtin_object_prototype; @@ -842,7 +832,7 @@ static size_t scr_dyn_key_slot(const char *key, size_t length) { return hash % SCR_DYN_KEY_SHARE_SLOTS; } -static char *scr_dyn_key_copy(const char *key, size_t length) { +static inline __attribute__((always_inline)) 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 && @@ -877,6 +867,246 @@ static void scr_dyn_key_release(char *key, size_t length) { free(owned); } +static inline void scr_dyn_key_retain(char *key) { + ((ScrDynKey *)(key - offsetof(ScrDynKey, bytes)))->rc++; +} + +/* ── layouts (scr_dyn.h) ─────────────────────────────────────────────── + * The thread's transition tree: the root is the empty key sequence, and a + * child appends one key to its parent's sequence. Transitions live in one + * open addressing table keyed by (parent, key); each parent also remembers + * its most recent child, which is all a loop building records of one + * shape ever needs. A chain of layouts shares one key array: a child + * appends its key to its parent's array when nothing was appended past the + * parent yet, and copies the parent's keys into a new array otherwise. + * + * A thread creates at most SCR_LAYOUT_BUDGET layouts holding at most + * SCR_LAYOUT_KEY_BYTES_BUDGET key bytes (and key arrays of at most + * SCR_LAYOUT_ARRAY_BYTES_BUDGET); past either, objects whose key sequence + * has no layout yet move to dictionary mode, and layouts that exist keep + * serving. Everything is freed with the thread's runtime state (at thread + * exit and when a library session ends). */ +#define SCR_LAYOUT_BUDGET ((size_t)1 << 16) +#define SCR_LAYOUT_KEY_BYTES_BUDGET ((size_t)4 << 20) +#define SCR_LAYOUT_ARRAY_BYTES_BUDGET ((size_t)16 << 20) +/* Layouts with more keys than this find keys through a hash index. */ +#define SCR_LAYOUT_LINEAR_MAX 8 + +typedef struct ScrLayoutKeys { + uint32_t used; + uint32_t cap; + ScrLayoutKey items[]; +} ScrLayoutKeys; + +static SCR_TL ScrLayout scr_layout_empty; +static SCR_TL ScrLayout *scr_layout_all; +static SCR_TL ScrLayout **scr_layout_transitions; +static SCR_TL size_t scr_layout_transition_mask; /* capacity - 1; 0 when absent */ +static SCR_TL size_t scr_layout_count; +static SCR_TL size_t scr_layout_key_bytes; +static SCR_TL size_t scr_layout_array_bytes; +static SCR_TL bool scr_layout_registered; + +const ScrLayout *scr_layout_root(void) { return &scr_layout_empty; } + +static void scr_layout_reset(void) { + while (scr_layout_all) { + ScrLayout *layout = scr_layout_all; + scr_layout_all = layout->all_next; + const ScrLayoutKey *key = &layout->keys[layout->count - 1]; + scr_dyn_key_release(key->key, key->len); + /* Layouts sharing a key array are newer than the one that owns it. */ + if (layout->owns_store) free(layout->store); + free(layout->lookup); + free(layout); + } + free(scr_layout_empty.lookup); + scr_layout_empty = (ScrLayout){0}; + free(scr_layout_transitions); + scr_layout_transitions = NULL; + scr_layout_transition_mask = 0; + scr_layout_count = 0; + scr_layout_key_bytes = 0; + scr_layout_array_bytes = 0; + scr_layout_registered = false; +} + +#ifdef SCR_RC_AUDIT +/* For the runtime's unit tests: frees the layouts while no object uses one. */ +void scr_layout_reset_idle(void) { scr_layout_reset(); } +#endif + +static size_t scr_layout_slot(const ScrLayout *parent, uint32_t hash) { + return (size_t)scr_key_mix((uint64_t)hash ^ (uint64_t)(uintptr_t)parent); +} + +static void scr_layout_insert(ScrLayout *child) { + size_t i = scr_layout_slot(child->parent, child->keys[child->count - 1].hash) & scr_layout_transition_mask; + while (scr_layout_transitions[i]) i = (i + 1) & scr_layout_transition_mask; + scr_layout_transitions[i] = child; +} + +static void scr_layout_transitions_grow(void) { + size_t old_cap = scr_layout_transition_mask ? scr_layout_transition_mask + 1 : 0; + size_t cap = old_cap ? old_cap * 2 : 256; + ScrLayout **old = scr_layout_transitions; + scr_layout_transitions = calloc(cap, sizeof *scr_layout_transitions); + if (!scr_layout_transitions) scr_json_oom(); + scr_layout_transition_mask = cap - 1; + for (size_t i = 0; i < old_cap; i++) + if (old[i]) scr_layout_insert(old[i]); + free(old); +} + +static inline bool scr_layout_key_is(const ScrLayoutKey *k, const char *key, size_t key_len) { + return k->len == key_len && (k->key == key || scr_key_equal(k->key, key, key_len)); +} + +static __attribute__((noinline)) void scr_layout_index(ScrLayout *layout) { + size_t cap = 16; + while (cap < 2 * (size_t)layout->count) cap *= 2; + uint8_t *table = calloc(cap, 1); + if (!table) scr_json_oom(); + for (uint32_t i = 0; i < layout->count; i++) { + size_t slot = layout->keys[i].hash & (cap - 1); + while (table[slot]) slot = (slot + 1) & (cap - 1); + table[slot] = (uint8_t)(i + 1); + } + layout->lookup = table; + layout->lookup_mask = (uint32_t)(cap - 1); +} + +uint32_t scr_layout_find(const ScrLayout *layout, const char *key, size_t key_len) { + uint32_t count = layout->count; + if (count <= SCR_LAYOUT_LINEAR_MAX) { + for (uint32_t i = 0; i < count; i++) + if (scr_layout_key_is(&layout->keys[i], key, key_len)) return i; + return UINT32_MAX; + } + if (!layout->lookup) scr_layout_index((ScrLayout *)layout); + uint32_t hash = (uint32_t)scr_key_hash(key, key_len); + size_t mask = layout->lookup_mask; + for (size_t slot = hash & mask;; slot = (slot + 1) & mask) { + uint8_t found = layout->lookup[slot]; + if (!found) return UINT32_MAX; + const ScrLayoutKey *k = &layout->keys[found - 1]; + if (k->hash == hash && scr_layout_key_is(k, key, key_len)) return found - 1u; + } +} + +const ScrLayout *scr_layout_add(const ScrLayout *layout, const char *key, size_t key_len) { + ScrLayout *parent = (ScrLayout *)layout; + ScrLayout *last = parent->last_child; + if (last && scr_layout_key_is(&last->keys[last->count - 1], key, key_len)) return last; + if (parent->count >= SCR_LAYOUT_MAX_KEYS || key_len > UINT32_MAX) return NULL; + uint32_t hash = (uint32_t)scr_key_hash(key, key_len); + if (scr_layout_transition_mask) { + size_t i = scr_layout_slot(parent, hash) & scr_layout_transition_mask; + for (ScrLayout *child; (child = scr_layout_transitions[i]); i = (i + 1) & scr_layout_transition_mask) { + const ScrLayoutKey *k = &child->keys[child->count - 1]; + if (child->parent == parent && k->hash == hash && scr_layout_key_is(k, key, key_len)) { + parent->last_child = child; + return child; + } + } + } + if (scr_layout_count >= SCR_LAYOUT_BUDGET || + key_len > SCR_LAYOUT_KEY_BYTES_BUDGET - scr_layout_key_bytes) + return NULL; + uint32_t count = parent->count + 1; + ScrLayoutKeys *store = parent->store; + bool owns = !store || store->used != parent->count || store->used == store->cap; + if (owns) { + uint32_t cap = count < 4 ? 4 : count * 2; + if (cap > SCR_LAYOUT_MAX_KEYS) cap = SCR_LAYOUT_MAX_KEYS; + size_t bytes = sizeof *store + cap * sizeof(ScrLayoutKey); + if (bytes > SCR_LAYOUT_ARRAY_BYTES_BUDGET - scr_layout_array_bytes) return NULL; + store = malloc(bytes); + if (!store) scr_json_oom(); + if (parent->count) memcpy(store->items, parent->keys, parent->count * sizeof(ScrLayoutKey)); + store->used = parent->count; + store->cap = cap; + scr_layout_array_bytes += bytes; + } + if ((scr_layout_count + 1) * 2 > (scr_layout_transition_mask ? scr_layout_transition_mask + 1 : 0)) + scr_layout_transitions_grow(); + ScrLayout *child = malloc(sizeof *child); + if (!child) scr_json_oom(); + store->items[store->used++] = (ScrLayoutKey){scr_dyn_key_copy(key, key_len), (uint32_t)key_len, hash}; + *child = (ScrLayout){store->items, count, 0, NULL, parent, NULL, store, scr_layout_all, owns}; + scr_layout_all = child; + if (!scr_layout_registered) { + scr_layout_registered = true; + scr_atexit(scr_layout_reset); + } + scr_layout_insert(child); + scr_layout_count++; + scr_layout_key_bytes += key_len; + parent->last_child = child; + return child; +} + +#define SCR_DYN_OBJECT_INDEX_MIN 32 + +/* Layout mode → dictionary mode: the same properties in the same order, + * each a writable, enumerable, configurable data property. */ +void scr_dyn_to_dictionary(ScrDyn *object) { + const ScrLayout *layout = object->v.obj.layout; + if (!layout) return; + size_t len = object->v.obj.len; + ScrDyn **slots = object->v.obj.slots; + size_t cap = len < 4 ? 4 : len; + ScrDynEntry *entries = scr_mem_realloc(NULL, cap * sizeof *entries); + if (!entries) scr_json_oom(); + memset(entries, 0, cap * sizeof *entries); + size_t traced = object->v.obj.slots_traced; + for (size_t i = 0; i < len; i++) { + const ScrLayoutKey *k = &layout->keys[i]; + scr_dyn_key_retain(k->key); + ScrDynEntry *e = &entries[i]; + e->key = k->key; + e->key_len = k->len; + e->value = slots[i]; + e->value_traced = (traced >> i) & 1; + e->writable = e->enumerable = e->configurable = true; + } + scr_mem_free(slots); + object->v.obj.entries = entries; + object->v.obj.cap = cap; + object->v.obj.layout = NULL; + object->v.obj.slots_traced = 0; + /* A wide object gets its hash index with its next insertion + * (scr_dyn_obj_put); until then lookups scan its entries. */ + object->v.obj.index = NULL; +} + +/* Appends a property `key` does not name yet to a layout-mode object, + * taking ownership of `value`; false (nothing done) when no layout fits, + * and the caller then takes the dictionary path. */ +/* Appends `value` (owned) to a layout-mode object as the slot of the last + * key of `next`, a child of the object's layout. */ +static inline void scr_dyn_obj_layout_push(ScrDyn *obj, const ScrLayout *next, ScrDyn *value) { + size_t len = obj->v.obj.len; + if (len == obj->v.obj.cap) { + size_t cap = obj->v.obj.cap ? obj->v.obj.cap * 2 : 4; + if (cap > SCR_LAYOUT_MAX_KEYS) cap = SCR_LAYOUT_MAX_KEYS; + ScrDyn **slots = scr_mem_realloc(obj->v.obj.slots, cap * sizeof *slots); + if (!slots) scr_json_oom(); + obj->v.obj.slots = slots; + obj->v.obj.cap = cap; + } + obj->v.obj.slots[len] = value; + obj->v.obj.len = len + 1; + obj->v.obj.layout = next; +} + +static inline bool scr_dyn_obj_layout_append(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value) { + const ScrLayout *next = scr_layout_add(obj->v.obj.layout, key, key_len); + if (!next) return false; + scr_dyn_obj_layout_push(obj, next, value); + return true; +} + #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; @@ -887,39 +1117,25 @@ static SCR_TL size_t scr_dyn_free_count; * arena never returns them to the system either), at the cost of reserving * them, with their item/entry buffers, for dyn nodes. */ #define SCR_DYN_FREE_MAX ((size_t)1 << 20) -/* Recycled small objects may retain key bytes, but never values or an - * unbounded document's keys. This budget applies across the whole pool. */ -static SCR_TL size_t scr_dyn_cached_key_bytes; -#define SCR_DYN_KEY_CACHE_MAX ((size_t)64 * 1024) -#define SCR_DYN_KEY_CACHE_SLOTS 16 -#define SCR_DYN_KEY_CACHE_LENGTH 64 #endif -/* Returns the key bytes it kept for reuse, which it added to the budget; - * the pooled node records them (v.obj.cached_key_bytes) so its reuse takes - * them back without walking its slots. */ -static size_t scr_dyn_obj_drop_keys(ScrDyn *d, bool cache) { - size_t kept = 0; - for (size_t i = 0; i < d->v.obj.cap; i++) { - ScrDynEntry *entry = &d->v.obj.entries[i]; - if (!entry->key) continue; -#ifndef SCR_RC_AUDIT - size_t bytes = sizeof(ScrDynKey) + entry->key_len + 1; - if (cache && d->v.obj.cap <= SCR_DYN_KEY_CACHE_SLOTS && - entry->key_len <= SCR_DYN_KEY_CACHE_LENGTH && - bytes <= SCR_DYN_KEY_CACHE_MAX - scr_dyn_cached_key_bytes) { - scr_dyn_cached_key_bytes += bytes; - kept += bytes; - continue; +/* Drops a disposed object's keys and storage: a dictionary's entries with the keys + * they hold (recycled entries past len may hold keys too), or a layout-mode + * object's slots unless `keep_slots` (a pooled node keeps them for its next + * life). Leaves the node empty, in layout mode's representation. */ +static void scr_dyn_obj_drop_keys(ScrDyn *d, bool keep_slots) { + if (d->v.obj.layout) { + if (keep_slots) return; + scr_mem_free(d->v.obj.slots); + } else { + for (size_t i = 0; i < d->v.obj.cap; i++) { + ScrDynEntry *entry = &d->v.obj.entries[i]; + if (entry->key) scr_dyn_key_release(entry->key, entry->key_len); } -#else - (void)cache; -#endif - scr_dyn_key_release(entry->key, entry->key_len); - entry->key = NULL; - entry->key_len = 0; + scr_mem_free(d->v.obj.entries); } - return kept; + d->v.obj.slots = NULL; + d->v.obj.cap = 0; } /* Primitive kinds cannot acquire properties or change kind while live, so @@ -948,6 +1164,17 @@ void scr_dyn_trace_v(void *ptr, ScrTraceVisit visit, void *ctx) { if (d->v.arr.properties) visit(d->v.arr.properties, ctx); break; case SCR_DYN_OBJ: + if (d->v.obj.layout) { + size_t traced = 0; + for (size_t i = 0; i < d->v.obj.len; i++) { + ScrDyn *value = d->v.obj.slots[i]; + if (!scr_dyn_member_is_traced(value)) continue; + traced |= (size_t)1 << i; + visit(value, ctx); + } + d->v.obj.slots_traced = traced; + break; + } for (size_t i = 0; i < d->v.obj.len; i++) { ScrDynEntry *entry = &d->v.obj.entries[i]; entry->value_traced = scr_dyn_member_is_traced(entry->value); @@ -1120,12 +1347,11 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) { d->v.arr.presence = NULL; d->v.arr.region = NULL; } else if (kind == SCR_DYN_OBJ) { - d->v.obj.len = 0; /* cap/entries preserved */ - /* The kept keys leave the pool's budget with the node. */ - scr_dyn_cached_key_bytes -= d->v.obj.cached_key_bytes; + d->v.obj.len = 0; /* cap/slots preserved */ d->v.obj.region = NULL; d->v.obj.class_name = NULL; /* a released class prototype's name */ - SCR_JS_SHAPE_RESET(d); + d->v.obj.layout = &scr_layout_empty; + d->v.obj.slots_traced = 0; d->v.obj.source_identity = NULL; d->v.obj.source_access = NULL; } else if (kind != SCR_DYN_NUM && kind != SCR_DYN_STR) { @@ -1144,9 +1370,10 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) { fresh->prototype = NULL; fresh->symbol_properties = NULL; fresh->symbol_keys = NULL; -#ifdef SCR_JS_TIER - if (kind == SCR_DYN_OBJ) SCR_JS_SHAPE_RESET(fresh); -#endif + if (kind == SCR_DYN_OBJ) { + memset(&fresh->v, 0, sizeof fresh->v); + fresh->v.obj.layout = &scr_layout_empty; + } #ifdef SCR_RC_AUDIT scr_live_dyns++; #endif @@ -1225,7 +1452,7 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { return; } #endif - if (d->kind == SCR_DYN_OBJ) { + if (d->kind == SCR_DYN_OBJ && !d->v.obj.layout) { free(d->v.obj.index); d->v.obj.index = NULL; } @@ -1259,7 +1486,12 @@ 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++) { + if (d->v.obj.layout) { + size_t traced = collected ? d->v.obj.slots_traced : 0; + for (size_t i = 0; i < d->v.obj.len; i++) + if (!((traced >> i) & 1)) scr_dyn_release_child(d->v.obj.slots[i]); + d->v.obj.len = 0; + } else 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_child(value); if (!collected) { @@ -1313,7 +1545,7 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { #endif #ifndef SCR_RC_AUDIT if (!collected && scr_dyn_free_count < SCR_DYN_FREE_MAX) { - if (d->kind == SCR_DYN_OBJ) d->v.obj.cached_key_bytes = scr_dyn_obj_drop_keys(d, true); + if (d->kind == SCR_DYN_OBJ) scr_dyn_obj_drop_keys(d, true); /* Pooled blocks retain their headers and count toward the bounded * allocated heap, but never remain in a candidate buffer. */ scr_dyn_weak_dispose(d); @@ -1327,10 +1559,7 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { } #endif if (d->kind == SCR_DYN_ARR) scr_mem_free(d->v.arr.items); - else if (d->kind == SCR_DYN_OBJ) { - scr_dyn_obj_drop_keys(d, false); - scr_mem_free(d->v.obj.entries); - } + else if (d->kind == SCR_DYN_OBJ) scr_dyn_obj_drop_keys(d, false); scr_cyc_free_inline(d); } @@ -1412,7 +1641,6 @@ void scr_dyn_release(ScrDyn *d) { /* Small objects retain their compact linear representation. A wide object * owns a bucket index of entry positions; entries remain insertion ordered. * No key/value ownership lives here, and callbacks never retain its slots. */ -#define SCR_DYN_OBJECT_INDEX_MIN 32 typedef struct ScrDynObjectIndex { size_t buckets; size_t slots[]; /* zero is empty; occupied slots store entry + 1 */ @@ -1472,8 +1700,12 @@ static __attribute__((noinline)) ScrDynEntry *scr_dyn_find_indexed_entry( return NULL; } +/* The entry of own property `key`. Its callers read or write entries, so + * a layout-mode object moves to dictionary mode first; paths that know + * layouts look up slots with scr_dyn_layout_slot instead. */ static ScrDynEntry *scr_dyn_find_entry(const ScrDyn *object, const char *key, size_t length) { if (object->kind != SCR_DYN_OBJ) return NULL; + if (object->v.obj.layout) scr_dyn_to_dictionary((ScrDyn *)object); if (object->v.obj.index) return scr_dyn_find_indexed_entry(object, key, length); size_t slot = (length + (length ? (unsigned char)key[0] * 7u : 0)) % SCR_DYN_PROPERTY_HINTS; size_t hint = scr_dyn_property_hints[slot]; @@ -1486,16 +1718,49 @@ static ScrDynEntry *scr_dyn_find_entry(const ScrDyn *object, const char *key, si return entry; } +/* Out-of-line property accessors for emitted object walks (the + * index-signature checks of backend/llvm/dyn.ts). */ +const char *scr_dyn_member_key(const ScrDyn *object, size_t i) { + size_t key_len; + return scr_dyn_obj_key_at(object, i, &key_len); +} + +size_t scr_dyn_member_key_len(const ScrDyn *object, size_t i) { + size_t key_len; + scr_dyn_obj_key_at(object, i, &key_len); + return key_len; +} + +ScrDyn *scr_dyn_member_value(const ScrDyn *object, size_t i) { + return scr_dyn_obj_value_at(object, i); +} + +/* The slot of own property `key` of a layout-mode object, or NULL. */ +static inline ScrDyn **scr_dyn_layout_slot(const ScrDyn *object, const char *key, size_t length) { + uint32_t i = scr_layout_find(object->v.obj.layout, key, length); + return i == UINT32_MAX ? NULL : &object->v.obj.slots[i]; +} + +/* Whether `object` is an ordinary object in layout mode. */ +static inline bool scr_dyn_is_layout(const ScrDyn *object) { + return object->kind == SCR_DYN_OBJ && object->v.obj.layout; +} + 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 (scr_dyn_is_layout(d)) { + ScrDyn **slot = scr_dyn_layout_slot(d, key, key_len); + return slot ? *slot : 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) { + if (scr_dyn_is_layout(d)) return scr_dyn_layout_slot(d, key, key_len) != NULL; ScrDynEntry *entry = scr_dyn_find_entry(d, key, key_len); return entry && entry->enumerable; } @@ -1539,8 +1804,18 @@ int32_t scr_dyn_discriminant_case(const ScrDyn *d, const ScrStr *key, ScrStr *co if (!d) return -1; if (d->kind == SCR_DYN_TYPED_REF) return -2; if (d->kind != SCR_DYN_OBJ) return -1; + if (d->v.obj.layout) { + ScrDyn **slot = scr_dyn_layout_slot(d, key->data, key->len); + if (!slot) return -2; + const ScrDyn *member = *slot; + if (!member || member->kind != SCR_DYN_STR) return -1; + for (size_t k = 0; k < count; k++) + if (scr_str_eq(member->v.str, values[k])) return (int32_t)k; + return -1; + } for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *entry = &d->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(d, i); + const ScrDynProp *entry = &entry_prop; if (entry->key_len != key->len || memcmp(entry->key, key->data, key->len) != 0) continue; if (entry->accessor) return -2; const ScrDyn *member = entry->value; @@ -1868,6 +2143,17 @@ ScrDyn *scr_dyn_arr_named_get(const ScrDyn *d, const ScrStr *key) { * A recycled entry can reuse equal key bytes without a malloc/free pair. */ static void scr_dyn_obj_put(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value) { scr_dyn_region_store(obj, value); + if (obj->v.obj.layout) { + ScrDyn **slot = scr_dyn_layout_slot(obj, key, key_len); + if (slot) { + ScrDyn *old = *slot; + *slot = value; + scr_dyn_release(old); + return; + } + if (!obj->non_extensible && scr_dyn_obj_layout_append(obj, key, key_len, value)) return; + scr_dyn_to_dictionary(obj); + } /* Construction usually appends an absent name. Avoid a read hint that * repeats comparisons on compact objects; wide objects use their index. */ ScrDynEntry *existing = obj->v.obj.index ? scr_dyn_find_entry(obj, key, key_len) @@ -1901,14 +2187,13 @@ static void scr_dyn_obj_put(ScrDyn *obj, const char *key, size_t key_len, ScrDyn } if (obj->v.obj.len == obj->v.obj.cap) { size_t cap = obj->v.obj.cap ? obj->v.obj.cap * 2 : 4; - ScrDynEntry *entries = scr_mem_realloc(obj->v.obj.entries, cap * sizeof *entries); + ScrDynEntry *entries = scr_mem_realloc(scr_dyn_obj_dict_entries(obj), cap * sizeof *entries); if (!entries) scr_json_oom(); memset(entries + obj->v.obj.cap, 0, (cap - obj->v.obj.cap) * sizeof *entries); obj->v.obj.entries = entries; obj->v.obj.cap = cap; } - SCR_JS_SHAPE_RESET(obj); - ScrDynEntry *e = &obj->v.obj.entries[obj->v.obj.len++]; + ScrDynEntry *e = &scr_dyn_obj_dict_entries(obj)[obj->v.obj.len++]; if (!e->key || e->key_len != key_len || !scr_key_equal(e->key, key, key_len)) { char *owned = scr_dyn_key_copy(key, key_len); scr_dyn_key_release(e->key, e->key_len); @@ -2550,7 +2835,7 @@ static void scr_dyn_record_view_freeze(ScrDyn *view) { view->non_extensible = true; if (view->kind == SCR_DYN_OBJ) { for (size_t i = 0; i < view->v.obj.len; i++) { - ScrDynEntry *entry = &view->v.obj.entries[i]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(view)[i]; entry->configurable = false; if (!entry->accessor) entry->writable = false; } @@ -2614,7 +2899,8 @@ double scr_dyn_class_view_integrity(const ScrDyn *view, double bits, ScrDyn *bag for (const ScrDyn *table = view; table; table = table == view ? view->symbol_properties : NULL) { if (table->kind != SCR_DYN_OBJ) continue; for (size_t i = 0; i < table->v.obj.len; i++) { - const ScrDynEntry *entry = &table->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(table, i); + const ScrDynProp *entry = &entry_prop; if (entry->configurable) sealed = false; if (!entry->accessor && entry->writable) frozen = false; } @@ -2795,7 +3081,7 @@ ScrDyn *scr_dyn_seal(ScrDyn *object) { } if (object->kind == SCR_DYN_OBJ) { object->non_extensible = true; - for (size_t i = 0; i < object->v.obj.len; i++) object->v.obj.entries[i].configurable = false; + for (size_t i = 0; i < object->v.obj.len; i++) scr_dyn_obj_dict_entries(object)[i].configurable = false; } if (object->kind == SCR_DYN_ARR) { object->non_extensible = true; @@ -2826,7 +3112,7 @@ bool scr_dyn_is_sealed(const ScrDyn *object) { if (object->kind == SCR_DYN_BYTES) return object->v.bytes->properties && scr_dyn_is_sealed(object->v.bytes->properties); if (object->kind != SCR_DYN_OBJ) return true; if (!object->non_extensible) return false; - for (size_t i = 0; i < object->v.obj.len; i++) if (object->v.obj.entries[i].configurable) return false; + for (size_t i = 0; i < object->v.obj.len; i++) if (scr_dyn_obj_prop_at(object, i).configurable) return false; return true; } @@ -2855,6 +3141,10 @@ static const ScrDyn *scr_dyn_property_owner(const ScrDyn *object, const char *ke return owner; } if (current->kind != SCR_DYN_OBJ) continue; + if (current->v.obj.layout) { + if (scr_layout_find(current->v.obj.layout, key, length) != UINT32_MAX) return current; + continue; + } for (size_t i = 0; i < current->v.obj.len; i++) { const ScrDynEntry *entry = ¤t->v.obj.entries[i]; if (entry->key_len == length && memcmp(entry->key, key, length) == 0) return current; @@ -2897,7 +3187,7 @@ ScrDyn *scr_dyn_proxy_new(const ScrDyn *target, const ScrDyn *handler) { ScrDyn *view = scr_dyn_typed_ref_materialize(target); bool plain = view && view->kind == SCR_DYN_OBJ; if (plain) for (size_t i = 0; i < view->v.obj.len; i++) { - ScrDynEntry *entry = &view->v.obj.entries[i]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(view)[i]; if (entry->accessor || !entry->configurable || !entry->writable) plain = false; } scr_dyn_release(view); @@ -3267,6 +3557,16 @@ ScrDyn *scr_dyn_own_descriptor(const ScrDyn *value, const ScrStr *key) { if (value->kind == SCR_DYN_BOOL || value->kind == SCR_DYN_NUM) { return scr_dyn_retain(scr_dyn_undefined()); } + if (value->kind == SCR_DYN_OBJ && value->v.obj.layout) { + ScrDyn **slot = scr_dyn_layout_slot(value, key->data, key->len); + if (!slot) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *out = scr_dyn_new_obj(); + scr_dyn_obj_set(out, "value", 5, scr_dyn_retain(*slot)); + scr_dyn_obj_set(out, "writable", 8, scr_dyn_new_bool(true)); + scr_dyn_obj_set(out, "enumerable", 10, scr_dyn_new_bool(true)); + scr_dyn_obj_set(out, "configurable", 12, scr_dyn_new_bool(true)); + return out; + } if (value->kind == SCR_DYN_OBJ) { ScrDynEntry *entry = scr_dyn_entry((ScrDyn *)value, key); if (!entry) return scr_dyn_retain(scr_dyn_undefined()); @@ -3626,7 +3926,8 @@ void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *nam bool keep = false; size_t next = 0; for (size_t i = 0; i < src->v.obj.len && !keep; i++) { - const ScrDynEntry *entry = &src->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(src, i); + const ScrDynProp *entry = &entry_prop; size_t index = scr_record_source_index(&probe, entry->key, entry->key_len); keep = index == count || index < next; next = index + 1; @@ -3634,7 +3935,8 @@ void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *nam if (!keep) return; ScrDyn *view = scr_dyn_new_obj(); for (size_t i = 0; i < src->v.obj.len; i++) { - const ScrDynEntry *entry = &src->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(src, i); + const ScrDynProp *entry = &entry_prop; if (scr_record_source_declared(&probe, entry->key, entry->key_len)) { scr_dyn_obj_set(view, entry->key, entry->key_len, scr_dyn_retain(scr_dyn_undefined())); } else if (!entry->accessor && entry->writable && entry->enumerable && entry->configurable) { @@ -3667,7 +3969,8 @@ void scr_record_source_spread(void *dst, const void *src, const char *const *nam bool keep = false; size_t next = 0; for (size_t i = 0; i < from->v.obj.len && !keep; i++) { - const ScrDynEntry *entry = &from->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(from, i); + const ScrDynProp *entry = &entry_prop; size_t index = scr_record_source_index(&probe, entry->key, entry->key_len); if (index == count) continue; keep = index != next; @@ -3676,7 +3979,8 @@ void scr_record_source_spread(void *dst, const void *src, const char *const *nam if (!keep) return; ScrDyn *view = scr_dyn_new_obj(); for (size_t i = 0; i < from->v.obj.len; i++) { - const ScrDynEntry *entry = &from->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(from, i); + const ScrDynProp *entry = &entry_prop; if (scr_record_source_declared(&probe, entry->key, entry->key_len)) scr_dyn_obj_set(view, entry->key, entry->key_len, scr_dyn_retain(scr_dyn_undefined())); } @@ -3715,7 +4019,8 @@ static ScrDyn *scr_record_source_merge(const ScrRecordSource *source, const ScrD const ScrDyn *fresh) { ScrDyn *out = scr_dyn_new_obj(); for (size_t i = 0; i < cached->v.obj.len; i++) { - const ScrDynEntry *entry = &cached->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(cached, i); + const ScrDynProp *entry = &entry_prop; ScrDyn *value = scr_dyn_obj_get(fresh, entry->key, entry->key_len); if (value) { scr_dyn_obj_set(out, entry->key, entry->key_len, scr_dyn_retain(value)); @@ -3736,7 +4041,8 @@ static ScrDyn *scr_record_source_merge(const ScrRecordSource *source, const ScrD scr_str_release(name); } for (size_t i = 0; i < fresh->v.obj.len; i++) { - const ScrDynEntry *entry = &fresh->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(fresh, i); + const ScrDynProp *entry = &entry_prop; if (scr_dyn_obj_get(out, entry->key, entry->key_len)) continue; scr_dyn_obj_set(out, entry->key, entry->key_len, scr_dyn_retain(entry->value)); } @@ -3812,7 +4118,8 @@ static void scr_record_source_key_order(const ScrDyn *view, size_t *out) { if (!keys) scr_json_oom(); size_t integers = 0, next = 0; for (size_t i = 0; i < len; i++) { - const ScrDynEntry *entry = &view->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(view, i); + const ScrDynProp *entry = &entry_prop; double index; if (scr_dyn_key_is_index(entry->key, entry->key_len, &index)) keys[integers++] = (ScrRecordSourceIntegerKey){ index, i }; @@ -3821,7 +4128,8 @@ static void scr_record_source_key_order(const ScrDyn *view, size_t *out) { qsort(keys, integers, sizeof *keys, scr_record_source_integer_cmp); for (size_t i = 0; i < integers; i++) out[next++] = keys[i].entry; for (size_t i = 0; i < len; i++) { - const ScrDynEntry *entry = &view->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(view, i); + const ScrDynProp *entry = &entry_prop; double index; if (!scr_dyn_key_is_index(entry->key, entry->key_len, &index)) out[next++] = i; } @@ -3854,7 +4162,7 @@ bool scr_jb_try_record_source(ScrJsonBuf *b, void *rec, size_t *order = NULL; for (size_t k = 0; k < view->v.obj.len && !order; k++) { double index; - if (!scr_dyn_key_is_index(view->v.obj.entries[k].key, view->v.obj.entries[k].key_len, &index)) continue; + if (!scr_dyn_key_is_index(scr_dyn_obj_dict_entries(view)[k].key, scr_dyn_obj_dict_entries(view)[k].key_len, &index)) continue; order = view->v.obj.len <= 64 ? order_small : malloc(view->v.obj.len * sizeof *order); if (!order) scr_json_oom(); scr_record_source_key_order(view, order); @@ -3865,7 +4173,8 @@ bool scr_jb_try_record_source(ScrJsonBuf *b, void *rec, * SerializeJSONObject takes them before reading any value. */ size_t i = 0; for (; i < view->v.obj.len && !scr_exc_pending(); i++) { - const ScrDynEntry *entry = &view->v.obj.entries[order ? order[i] : i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(view, order ? order[i] : i); + const ScrDynProp *entry = &entry_prop; size_t index = scr_record_source_index(&source, entry->key, entry->key_len); if (index < count) { if (!seen[index] && scr_jb_put_source_field(b, rec, &source, index, first, on_stack, field)) @@ -3885,7 +4194,8 @@ bool scr_jb_try_record_source(ScrJsonBuf *b, void *rec, if (i < view->v.obj.len && !scr_exc_pending()) { ScrDyn *rest = scr_dyn_new_arr(); for (size_t j = i; j < view->v.obj.len; j++) { - const ScrDynEntry *entry = &view->v.obj.entries[order ? order[j] : j]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(view, order ? order[j] : j); + const ScrDynProp *entry = &entry_prop; if (!entry->enumerable && !scr_record_source_declared(&source, entry->key, entry->key_len)) continue; ScrStr *name = scr_str_new(entry->key, entry->key_len); scr_dyn_arr_push(rest, scr_dyn_new_str(name)); @@ -4038,7 +4348,7 @@ static void scr_dyn_typed_ref_preserve_children( } if (cached->kind == SCR_DYN_OBJ) { for (size_t i = 0; i < fresh->v.obj.len; i++) { - ScrDynEntry *entry = &fresh->v.obj.entries[i]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(fresh)[i]; ScrDyn *cached_child = scr_dyn_obj_get( cached, entry->key, entry->key_len); scr_dyn_typed_ref_preserve_child(&entry->value, cached_child); @@ -4086,7 +4396,7 @@ static void scr_typed_ref_view_numeric_properties(const ScrDyn *capsule, ScrDyn if (value) scr_dyn_arr_named_put(view, x->props[i].key, x->props[i].key_len, value); } if (!named) return; - for (size_t i = 0; i < named->v.obj.len; i++) scr_dyn_copy_entry(view, named, &named->v.obj.entries[i]); + for (size_t i = 0; i < named->v.obj.len; i++) scr_dyn_copy_entry(view, named, &scr_dyn_obj_dict_entries(named)[i]); scr_dyn_release(named); } @@ -4121,7 +4431,7 @@ ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) { fresh = ordered; } else if (record_view) { for (size_t i = 0; i < cached->v.obj.len; i++) { - ScrDynEntry *entry = &cached->v.obj.entries[i]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(cached)[i]; if (scr_dyn_obj_get(fresh, entry->key, entry->key_len)) continue; ScrStr *name = scr_str_new(entry->key, entry->key_len); ScrDyn *key = scr_dyn_new_str(name); @@ -4855,16 +5165,16 @@ static ScrDyn *scr_bytes_constructor_for(ScrBytesElem elem) { scr_bytes_ctor_call, 3, "typed-array-constructor", scr_bytes_elem_name(elem)); ScrDyn *properties = scr_dyn_fn_properties(scr_bytes_ctors[elem]); scr_dyn_obj_set(properties, "BYTES_PER_ELEMENT", 17, scr_dyn_new_num((double)scr_bytes_elem_size(elem))); - ScrDynEntry *entry = &properties->v.obj.entries[properties->v.obj.len - 1]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(properties)[properties->v.obj.len - 1]; entry->writable = entry->enumerable = entry->configurable = false; /* from and of live on %TypedArray%, which is not modeled: each * constructor carries them as non-enumerable properties. */ scr_dyn_obj_set(properties, "from", 4, scr_dyn_new_func(scr_closure_new(NULL, 0), scr_bytes_from_call, 1, "native:TypedArray", "from")); - properties->v.obj.entries[properties->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(properties)[properties->v.obj.len - 1].enumerable = false; scr_dyn_obj_set(properties, "of", 2, scr_dyn_new_func(scr_closure_new(NULL, 0), scr_bytes_of_call, 0, "native:TypedArray", "of")); - properties->v.obj.entries[properties->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(properties)[properties->v.obj.len - 1].enumerable = false; scr_dyn_release(properties); } return scr_dyn_retain(scr_bytes_ctors[elem]); @@ -5937,7 +6247,7 @@ ScrDyn *scr_dyn_class_prototype(ScrDyn *value, ScrDyn *base) { * `constructor`: the class value. The cycle it closes through the class * object is one the collector traces. */ scr_dyn_obj_set(data, "constructor", 11, scr_dyn_retain(value)); - data->v.obj.entries[data->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(data)[data->v.obj.len - 1].enumerable = false; cls->prototype_data = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); scr_box_set_ref(cls->prototype_data, data); } @@ -6069,7 +6379,7 @@ const char *scr_dyn_specific_type(const ScrDyn *cb, char *detail, size_t cap) { if (cb->null_proto) { /* No constructor name: Node renders inspect(value, { depth: -1 }). */ bool keys = false; - for (size_t i = 0; i < cb->v.obj.len && !keys; i++) keys = cb->v.obj.entries[i].enumerable; + for (size_t i = 0; i < cb->v.obj.len && !keys; i++) keys = scr_dyn_obj_dict_entries(cb)[i].enumerable; d = keys ? "[Object: null prototype]" : "[Object: null prototype] {}"; } else { d = "an instance of Object"; @@ -6717,6 +7027,21 @@ void scr_dyn_obj_set_accessor(ScrDyn *obj, const char *key, size_t key_len, ScrD * attributes copied into it (a frozen instance's view is built the same way * as any other). Consumes value. */ void scr_dyn_obj_set_field(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value) { + if (obj->v.obj.layout) { + ScrDyn **slot = scr_dyn_layout_slot(obj, key, key_len); + if (slot) { + scr_dyn_region_store(obj, value); + ScrDyn *old = *slot; + *slot = value; + scr_dyn_release(old); + return; + } + bool non_extensible = obj->non_extensible; + obj->non_extensible = false; + scr_dyn_obj_put(obj, key, key_len, value); + obj->non_extensible = non_extensible; + return; + } ScrDynEntry *existing = obj->v.obj.index ? scr_dyn_find_entry(obj, key, key_len) : scr_dyn_find_linear_entry(obj, key, key_len); if (existing && existing->fenced) { @@ -6822,8 +7147,8 @@ static ScrDyn *scr_dyn_fn_properties(const ScrDyn *function) { scr_dyn_obj_set(table, "name", 4, scr_dyn_new_str(name_string)); scr_str_release(name_string); for (size_t i = 0; i < table->v.obj.len; i++) { - table->v.obj.entries[i].writable = false; - table->v.obj.entries[i].enumerable = false; + scr_dyn_obj_dict_entries(table)[i].writable = false; + scr_dyn_obj_dict_entries(table)[i].enumerable = false; } ScrBox *box = scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, &scr_dyn_trace_v); scr_box_set_ref(box, table); @@ -6832,9 +7157,9 @@ static ScrDyn *scr_dyn_fn_properties(const ScrDyn *function) { if (owner->function_kind & 4) { ScrDyn *prototype = scr_dyn_new_obj(); scr_dyn_obj_set(prototype, "constructor", 11, scr_dyn_retain((ScrDyn *)function)); - prototype->v.obj.entries[0].enumerable = false; + scr_dyn_obj_dict_entries(prototype)[0].enumerable = false; scr_dyn_obj_set(table, "prototype", 9, prototype); - ScrDynEntry *entry = &table->v.obj.entries[table->v.obj.len - 1]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(table)[table->v.obj.len - 1]; entry->enumerable = false; entry->configurable = false; } @@ -7524,7 +7849,7 @@ static ScrDyn *scr_native_iterator_new(ScrDyn *source) { } #ifdef SCR_JS_TIER -/* A read site's answer for a receiver without a JS shape whose property is +/* A read site's answer for a receiver without a layout whose property is * plain data the runtime holds directly: a native array's length (the * generic read would build its checked view first) and a native iterator's * `next` (the builtin the generic read returns). +1, or NULL for the slow @@ -9636,7 +9961,7 @@ static void scr_errdyn_teardown(void); static size_t scr_errdyn_self_refs(const ScrDyn *d) { if (d->kind != SCR_DYN_OBJ) return 0; size_t n = 0; - for (size_t i = 0; i < d->v.obj.len; i++) n += d->v.obj.entries[i].value == d; + for (size_t i = 0; i < d->v.obj.len; i++) n += scr_dyn_obj_dict_entries(d)[i].value == d; return n; } @@ -9644,7 +9969,7 @@ static size_t scr_errdyn_self_refs(const ScrDyn *d) { * first so the release frees it. */ static void scr_errdyn_release_node(ScrDyn *d) { for (size_t i = 0; d->kind == SCR_DYN_OBJ && i < d->v.obj.len; i++) { - ScrDynEntry *entry = &d->v.obj.entries[i]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(d)[i]; if (entry->value != d) continue; entry->value = scr_dyn_retain(scr_dyn_undefined()); scr_dyn_release(d); @@ -9819,9 +10144,9 @@ static ScrDyn *scr_error_stack_get_call(ScrClosure *closure, ScrDyn *const *args static ScrDyn *scr_error_stack_set_call(ScrClosure *closure, ScrDyn *const *args, size_t argc); /* Whether an own property is an error's `stack` accessor. */ -static bool scr_dyn_entry_is_error_stack(const ScrDynEntry *entry) { - return entry->accessor && entry->setter && entry->setter->kind == SCR_DYN_FUNC && - entry->setter->v.fn.thunk == scr_error_stack_set_call; +static bool scr_dyn_setter_is_error_stack(bool accessor, const ScrDyn *setter) { + return accessor && setter && setter->kind == SCR_DYN_FUNC && + setter->v.fn.thunk == scr_error_stack_set_call; } static ScrDyn *scr_error_stack_set_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { @@ -9838,7 +10163,7 @@ static ScrDyn *scr_error_stack_set_call(ScrClosure *closure, ScrDyn *const *args ScrStr *key = scr_str_new("stack", 5); ScrDynEntry *entry = scr_dyn_entry(self, key); scr_str_release(key); - if (entry && scr_dyn_entry_is_error_stack(entry)) { + if (entry && scr_dyn_setter_is_error_stack(entry->accessor, entry->setter)) { ScrDyn *old_value = entry->value, *old_getter = entry->getter, *old_setter = entry->setter; entry->value = scr_dyn_retain(value); entry->getter = entry->setter = NULL; @@ -9866,7 +10191,7 @@ static void scr_error_dyn_add_stack(ScrDyn *d) { scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_stack_set_call, 1, "native:error.stack", NULL); } scr_dyn_obj_set(d, "stack", 5, scr_dyn_retain(scr_dyn_undefined())); - ScrDynEntry *entry = &d->v.obj.entries[d->v.obj.len - 1]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1]; entry->accessor = true; entry->getter = scr_dyn_retain(scr_error_stack_accessors[0]); entry->setter = scr_dyn_retain(scr_error_stack_accessors[1]); @@ -9889,25 +10214,25 @@ static bool scr_dyn_error_view_on_error_prototype(const ScrDyn *object) { ScrDyn *scr_error_dyn_fields(const ScrError *e) { ScrDyn *d = scr_dyn_new_obj(); scr_dyn_obj_set(d, "%error", 6, scr_dyn_new_bool(true)); /* the checked-dynamic tree's error marker */ - d->v.obj.entries[d->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1].enumerable = false; /* Node lists `stack` first among an error's own properties. */ scr_error_dyn_add_stack(d); if (!e->name_present || !e->message_present) { ScrDyn *prototype = scr_dyn_new_obj(); if (!e->name_present) { scr_dyn_obj_set(prototype, "name", 4, scr_dyn_new_str(e->name)); - prototype->v.obj.entries[prototype->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(prototype)[prototype->v.obj.len - 1].enumerable = false; } if (!e->message_present) { scr_dyn_obj_set(prototype, "message", 7, scr_dyn_new_str(e->message)); - prototype->v.obj.entries[prototype->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(prototype)[prototype->v.obj.len - 1].enumerable = false; } scr_dyn_release(scr_dyn_set_prototype(d, prototype)); scr_dyn_release(prototype); } if (e->message_present) { scr_dyn_obj_set(d, "message", 7, scr_dyn_new_str(e->message)); - d->v.obj.entries[d->v.obj.len - 1].enumerable = e->message_enumerable; + scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1].enumerable = e->message_enumerable; } if (e->system_message) { /* SystemError: code, the read-only `info` context ({ message, path, @@ -9921,7 +10246,7 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) { scr_dyn_obj_set(info, "errno", 5, scr_dyn_new_num(e->system_errno)); if (e->system_code) scr_dyn_obj_set(info, "code", 4, scr_dyn_new_str(e->system_code)); scr_dyn_obj_set(d, "info", 4, info); - d->v.obj.entries[d->v.obj.len - 1].writable = false; + scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1].writable = false; scr_dyn_obj_set(d, "errno", 5, scr_dyn_new_num(e->system_errno)); if (e->system_call) scr_dyn_obj_set(d, "syscall", 7, scr_dyn_new_str(e->system_call)); } else if (e->errno_order) { @@ -9947,7 +10272,7 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) { if (e->system_dest) scr_dyn_obj_set(d, "dest", 4, scr_dyn_new_str(e->system_dest)); if (e->error_cause) { scr_dyn_obj_set(d, "cause", 5, scr_dyn_retain(e->error_cause)); - d->v.obj.entries[d->v.obj.len - 1].enumerable = e->cause_enumerable; + scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1].enumerable = e->cause_enumerable; } /* DOMException: `code` is the WebIDL legacy NUMBER (never the errno * string slot), and the options form's cause crosses as itself. */ @@ -9963,20 +10288,20 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) { if (e->vt == &scr_error_vts[SCR_ERR_SUPPRESSED] && scr_exc_suppressed_values(e, &suppressed_error, &suppressed_value)) { scr_dyn_obj_set(d, "error", 5, scr_caught_to_dyn(suppressed_error)); - d->v.obj.entries[d->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1].enumerable = false; scr_dyn_obj_set(d, "suppressed", 10, scr_caught_to_dyn(suppressed_value)); - d->v.obj.entries[d->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1].enumerable = false; } /* AggregateError's own non-enumerable `errors` list, shared with the * typed read so both spellings answer the same array. */ if (e->vt == &scr_error_vts[SCR_ERR_AGGREGATE]) { scr_dyn_obj_set(d, "errors", 6, scr_aggregate_errors((ScrError *)e)); - d->v.obj.entries[d->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1].enumerable = false; } /* An own `name` is assigned after construction defines the others. */ if (e->name_present) { scr_dyn_obj_set(d, "name", 4, scr_dyn_new_str(e->name)); - d->v.obj.entries[d->v.obj.len - 1].enumerable = e->name_enumerable; + scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1].enumerable = e->name_enumerable; } return d; } @@ -9987,7 +10312,7 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) { * has no own `name` or no `after` property. */ void scr_error_dyn_place_name(ScrDyn *view, const char *after, size_t after_len) { if (view->kind != SCR_DYN_OBJ) return; - ScrDynEntry *entries = view->v.obj.entries; + ScrDynEntry *entries = scr_dyn_obj_dict_entries(view); size_t n = view->v.obj.len, from = n, to = n; for (size_t i = 0; i < n; i++) { if (entries[i].key_len == 4 && memcmp(entries[i].key, "name", 4) == 0) from = i; @@ -10000,14 +10325,14 @@ void scr_error_dyn_place_name(ScrDyn *view, const char *after, size_t after_len) memmove(&entries[from], &entries[from + 1], (n - from - 1) * sizeof *entries); memmove(&entries[at + 1], &entries[at], (n - 1 - at) * sizeof *entries); entries[at] = name; - SCR_JS_SHAPE_RESET(view); free(view->v.obj.index); view->v.obj.index = NULL; } static size_t scr_dyn_entry_index(const ScrDyn *object, const char *key, size_t key_len) { for (size_t i = 0; i < object->v.obj.len; i++) { - const ScrDynEntry *entry = &object->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(object, i); + const ScrDynProp *entry = &entry_prop; if (entry->key_len == key_len && memcmp(entry->key, key, key_len) == 0) return i; } return SIZE_MAX; @@ -10018,7 +10343,8 @@ static size_t scr_dyn_entry_index(const ScrDyn *object, const char *key, size_t * starting with '%'), and a `message` that directly follows them. */ static bool scr_error_bag_constructed(const ScrDyn *bag, size_t b) { for (size_t i = 0; i <= b; i++) { - const ScrDynEntry *entry = &bag->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(bag, i); + const ScrDynProp *entry = &entry_prop; bool constructed = (entry->key_len == 5 && memcmp(entry->key, "stack", 5) == 0) || (entry->key_len > 0 && entry->key[0] == '%') || (entry->key_len == 7 && memcmp(entry->key, "message", 7) == 0); @@ -10040,7 +10366,8 @@ static size_t scr_error_bag_place(const ScrDyn *view, const ScrDyn *bag, size_t bool name_placed, size_t from) { bool late = name || !scr_error_bag_constructed(bag, b); for (size_t p = b; p-- > 0;) { - const ScrDynEntry *anchor = &bag->v.obj.entries[p]; + const ScrDynProp anchor_prop = scr_dyn_obj_prop_at(bag, p); + const ScrDynProp *anchor = &anchor_prop; if (late && scr_error_bag_constructed(bag, p)) break; if (late && name_placed && anchor->key_len == 4 && memcmp(anchor->key, "name", 4) == 0) continue; size_t after = scr_dyn_entry_index(view, anchor->key, anchor->key_len); @@ -10048,7 +10375,7 @@ static size_t scr_error_bag_place(const ScrDyn *view, const ScrDyn *bag, size_t } if (!late) return 0; for (size_t p = b + 1; p < bag->v.obj.len; p++) { - size_t before = scr_dyn_entry_index(view, bag->v.obj.entries[p].key, bag->v.obj.entries[p].key_len); + size_t before = scr_dyn_entry_index(view, scr_dyn_obj_dict_entries(bag)[p].key, scr_dyn_obj_dict_entries(bag)[p].key_len); if (before != SIZE_MAX) return before > from ? before - 1 : before; } return from; @@ -10068,16 +10395,17 @@ void scr_error_dyn_follow_bag(ScrDyn *view, const ScrDyn *bag, const ScrError *e bool name = s == 0; if (!(name ? e->name_present : e->message_present)) continue; size_t at = scr_dyn_entry_index(view, slots[s].key, slots[s].len); - if (at == SIZE_MAX || view->v.obj.entries[at].accessor) continue; + if (at == SIZE_MAX || scr_dyn_obj_dict_entries(view)[at].accessor) continue; ScrDyn *value = scr_dyn_new_str(name ? e->name : e->message); scr_dyn_region_store(view, value); - ScrDyn *old = view->v.obj.entries[at].value; - view->v.obj.entries[at].value = value; + ScrDyn *old = scr_dyn_obj_dict_entries(view)[at].value; + scr_dyn_obj_dict_entries(view)[at].value = value; scr_dyn_release(old); } bool moved = false; for (size_t b = 0; b < bag->v.obj.len; b++) { - const ScrDynEntry *bag_entry = &bag->v.obj.entries[b]; + const ScrDynProp bag_entry_prop = scr_dyn_obj_prop_at(bag, b); + const ScrDynProp *bag_entry = &bag_entry_prop; bool slot = (bag_entry->key_len == 4 && memcmp(bag_entry->key, "name", 4) == 0) || (bag_entry->key_len == 7 && memcmp(bag_entry->key, "message", 7) == 0); bool name = bag_entry->key_len == 4; @@ -10087,7 +10415,7 @@ void scr_error_dyn_follow_bag(ScrDyn *view, const ScrDyn *bag, const ScrError *e if (from == SIZE_MAX) continue; size_t to = scr_error_bag_place(view, bag, b, name, name_placed, from); if (to == from) continue; - ScrDynEntry *entries = view->v.obj.entries; + ScrDynEntry *entries = scr_dyn_obj_dict_entries(view); ScrDynEntry entry = entries[from]; if (to < from) memmove(&entries[to + 1], &entries[to], (from - to) * sizeof *entries); else memmove(&entries[from], &entries[from + 1], (to - from) * sizeof *entries); @@ -10095,7 +10423,6 @@ void scr_error_dyn_follow_bag(ScrDyn *view, const ScrDyn *bag, const ScrError *e moved = true; } if (moved) { - SCR_JS_SHAPE_RESET(view); free(view->v.obj.index); view->v.obj.index = NULL; } @@ -10194,7 +10521,8 @@ void scr_error_commit_dyn(ScrError *e, const ScrDyn *view) { e->error_cause = replacement; e->cause_enumerable = false; for (size_t i = 0; cause && i < view->v.obj.len; i++) { - const ScrDynEntry *entry = &view->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(view, i); + const ScrDynProp *entry = &entry_prop; if (entry->key_len == 5 && memcmp(entry->key, "cause", 5) == 0) e->cause_enumerable = entry->enumerable; } } @@ -10228,7 +10556,8 @@ ScrError *scr_error_from_dyn(const ScrDyn *d) { scr_error_install_cause_drop(&scr_error_cause_drop_impl); e->error_cause = scr_dyn_retain(cause); for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *entry = &d->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(d, i); + const ScrDynProp *entry = &entry_prop; if (entry->key_len == 5 && memcmp(entry->key, "cause", 5) == 0) e->cause_enumerable = entry->enumerable; } } @@ -11364,7 +11693,18 @@ static void scr_dyn_throw_property(const ScrDyn *object, const char *key, size_t * is written (or runs its setter, or refuses the write); an absent one * consults the prototype chain and is then added. */ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, const ScrDyn *receiver) { - ScrDynEntry *entry = scr_dyn_entry(recv, key); + if (recv->v.obj.layout) { + ScrDyn **slot = scr_dyn_layout_slot(recv, key->data, key->len); + if (slot) { + scr_dyn_region_store(recv, value); + ScrDyn *old = *slot; + *slot = scr_dyn_retain(value); + scr_dyn_release(old); + scr_error_sync_cause(recv, key); + return; + } + } + ScrDynEntry *entry = recv->v.obj.layout ? NULL : scr_dyn_entry(recv, key); if (entry) { if (entry->accessor && entry->setter) { ScrDyn *args[] = {value}; @@ -11396,6 +11736,13 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con ScrDyn *table = current->kind == SCR_DYN_FUNC ? scr_dyn_fn_properties(current) : NULL; const ScrDyn *object = table ? table : current; if (object->kind != SCR_DYN_OBJ) { scr_dyn_release(table); break; } + if (object->v.obj.layout) { + /* A layout-mode prototype's properties are writable data. */ + bool found = scr_layout_find(object->v.obj.layout, key->data, key->len) != UINT32_MAX; + current = found ? NULL : scr_dyn_proto_next(object); + scr_dyn_release(table); + continue; + } ScrDynEntry *inherited = scr_dyn_entry((ScrDyn *)object, key); if (!inherited) { current = scr_dyn_proto_next(object); @@ -12507,6 +12854,13 @@ static void scr_json_fit_array(ScrDyn *arr) { static void scr_json_fit_object(ScrDyn *obj) { size_t len = obj->v.obj.len, cap = obj->v.obj.cap; if (len == 0 || len == cap) return; + if (obj->v.obj.layout) { + ScrDyn **slots = scr_json_fit_block(obj->v.obj.slots, len * sizeof *slots); + if (!slots) return; /* keep the larger block */ + obj->v.obj.slots = slots; + obj->v.obj.cap = len; + return; + } for (size_t i = len; i < cap; i++) { ScrDynEntry *entry = &obj->v.obj.entries[i]; scr_dyn_key_release(entry->key, entry->key_len); @@ -12570,6 +12924,7 @@ static ScrDyn *scr_json_array(ScrJsonP *p) { typedef struct ScrJsonShape { char *key; size_t key_len; + const ScrLayout *layout; /* the object's layout after this transition */ uint32_t child; /* first transition; 0 = none (the root is never a child) */ uint32_t sibling; /* next transition from the same layout; 0 = none */ } ScrJsonShape; @@ -12603,13 +12958,14 @@ static inline uint32_t scr_json_shape_follow(ScrJsonP *p, uint32_t shape) { return 0; } -/* Record `entry`'s key as a transition from `shape`; 0 when the budget is - * spent (the object then finishes on the ordinary path). */ -static uint32_t scr_json_shape_add(ScrJsonP *p, uint32_t shape, const ScrDynEntry *entry) { +/* Record the last key of `layout`, an object's layout after a put appended + * it, as a transition from `shape`; 0 when the budget is spent (the object + * then finishes on the ordinary path). */ +static uint32_t scr_json_shape_add(ScrJsonP *p, uint32_t shape, const ScrLayout *layout) { if (!p->shapes) { p->shapes = malloc(16 * sizeof *p->shapes); if (!p->shapes) scr_json_oom(); - p->shapes[0] = (ScrJsonShape){0}; + p->shapes[0] = (ScrJsonShape){NULL, 0, &scr_layout_empty, 0, 0}; p->nshapes = 1; p->shape_cap = 16; } @@ -12621,41 +12977,18 @@ static uint32_t scr_json_shape_add(ScrJsonP *p, uint32_t shape, const ScrDynEntr p->shape_cap *= 2; } uint32_t added = p->nshapes++; - ((ScrDynKey *)(entry->key - offsetof(ScrDynKey, bytes)))->rc++; - p->shapes[added] = (ScrJsonShape){entry->key, entry->key_len, 0, p->shapes[shape].child}; + const ScrLayoutKey *key = &layout->keys[layout->count - 1]; + scr_dyn_key_retain(key->key); + p->shapes[added] = (ScrJsonShape){key->key, key->len, layout, 0, p->shapes[shape].child}; p->shapes[shape].child = added; return added; } -/* scr_dyn_obj_put for a fresh parser object and a key it does not hold yet, - * sharing the transition's key bytes. */ -static void scr_dyn_obj_append_shared(ScrDyn *obj, char *key, size_t key_len, ScrDyn *value) { +/* scr_dyn_obj_put for a fresh parser object in layout mode and a key it + * does not hold yet: the transition's layout extends the object's. */ +static void scr_dyn_obj_append_shared(ScrDyn *obj, const ScrLayout *layout, ScrDyn *value) { scr_dyn_region_store(obj, value); - if (obj->v.obj.len == obj->v.obj.cap) { - size_t cap = obj->v.obj.cap ? obj->v.obj.cap * 2 : 4; - ScrDynEntry *entries = scr_mem_realloc(obj->v.obj.entries, cap * sizeof *entries); - if (!entries) scr_json_oom(); - memset(entries + obj->v.obj.cap, 0, (cap - obj->v.obj.cap) * sizeof *entries); - obj->v.obj.entries = entries; - obj->v.obj.cap = cap; - } - SCR_JS_SHAPE_RESET(obj); - ScrDynEntry *e = &obj->v.obj.entries[obj->v.obj.len++]; - if (e->key != key) { - ((ScrDynKey *)(key - offsetof(ScrDynKey, bytes)))->rc++; - scr_dyn_key_release(e->key, e->key_len); - e->key = key; - } - e->key_len = key_len; - e->value = value; - e->getter = NULL; - e->setter = NULL; - e->accessor = false; - e->fenced = false; - e->writable = true; - e->enumerable = true; - e->configurable = true; - if (obj->v.obj.len >= SCR_DYN_OBJECT_INDEX_MIN) scr_dyn_index_append(obj); + scr_dyn_obj_layout_push(obj, layout, value); } static ScrDyn *scr_json_object(ScrJsonP *p) { @@ -12730,14 +13063,14 @@ static ScrDyn *scr_json_object(ScrJsonP *p) { return NULL; } if (shared_key) { - scr_dyn_obj_append_shared(obj, shared_key, key_len, value); + scr_dyn_obj_append_shared(obj, p->shapes[follow].layout, value); shape = follow; } else { size_t before = obj->v.obj.len; scr_dyn_obj_put(obj, key, key_len, value); /* later duplicate keys win */ if (shaped) { - uint32_t added = !decoded_key && obj->v.obj.len == before + 1 - ? scr_json_shape_add(p, shape, &obj->v.obj.entries[before]) : 0; + uint32_t added = !decoded_key && obj->v.obj.len == before + 1 && obj->v.obj.layout + ? scr_json_shape_add(p, shape, obj->v.obj.layout) : 0; if (added) shape = added; else shaped = false; } @@ -13548,7 +13881,7 @@ static ScrDyn *scr_json_member(const ScrDyn *holder, const ScrStr *key) { static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) { for (size_t i = 0; i < object->v.obj.len; i++) { - ScrDynEntry *entry = &object->v.obj.entries[i]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(object)[i]; if (entry->key_len != key->len || memcmp(entry->key, key->data, key->len) != 0) continue; if (entry->fenced) { scr_dyn_namespace_member_fence(); @@ -13557,12 +13890,11 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) { ScrDynEntry removed = *entry; memmove(entry, entry + 1, (object->v.obj.len - i - 1) * sizeof *entry); object->v.obj.len--; - SCR_JS_SHAPE_RESET(object); free(object->v.obj.index); object->v.obj.index = NULL; /* 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); + memset(&scr_dyn_obj_dict_entries(object)[object->v.obj.len], 0, sizeof *entry); scr_dyn_key_release(removed.key, removed.key_len); scr_dyn_release(removed.value); scr_dyn_release(removed.getter); @@ -13937,7 +14269,8 @@ static bool scr_json_plain_container(const ScrDyn *value) { } if (value->kind != SCR_DYN_OBJ) return false; for (size_t i = 0; i < value->v.obj.len; i++) { - const ScrDynEntry *entry = &value->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(value, i); + const ScrDynProp *entry = &entry_prop; if (!entry->enumerable) continue; double index; if (entry->accessor || (!intact && !scr_json_plain_value(entry->value)) || @@ -13960,7 +14293,8 @@ static __attribute__((noinline)) bool scr_json_plain_write(ScrJsonBuf *buffer, c scr_jb_putc(buffer, array ? '[' : '{'); bool first = true; for (size_t i = 0; i < length; i++) { - const ScrDynEntry *entry = array ? NULL : &value->v.obj.entries[i]; + const ScrDynProp entry_prop = array ? (ScrDynProp){0} : scr_dyn_obj_prop_at(value, i); + const ScrDynProp *entry = array ? NULL : &entry_prop; if (entry && !entry->enumerable) continue; const ScrDyn *child = array ? value->v.arr.items[i] : entry->value; bool omitted = scr_json_omitted(child); @@ -14296,7 +14630,8 @@ static bool scr_jb_put_pure(ScrJsonBuf *b, const ScrDyn *value, size_t depth) { scr_jb_putc(b, '{'); bool first = true; for (size_t i = 0; i < value->v.obj.len; i++) { - const ScrDynEntry *entry = &value->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(value, i); + const ScrDynProp *entry = &entry_prop; if (!entry->enumerable) continue; double index; if (entry->accessor || (entry->key_len == 6 && memcmp(entry->key, "toJSON", 6) == 0) || @@ -14827,14 +15162,15 @@ static ScrDyn *scr_sc_clone(const ScrDyn *v, const ScrScParent *up, ScrDynRegion } ScrDyn *out = scr_dyn_new_obj(); for (size_t i = 0; i < v->v.obj.len; i++) { - const ScrDynEntry *e = &v->v.obj.entries[i]; - if (scr_dyn_entry_is_error_stack(e)) { + const ScrDynProp e_prop = scr_dyn_obj_prop_at(v, i); + const ScrDynProp *e = &e_prop; + if (scr_dyn_setter_is_error_stack(e->accessor, e->setter)) { /* A stack that was already formatted copies as a non-enumerable * data property; an unformatted one has nothing to copy. */ ScrError *error = scr_errdyn_err_of(v); if (error && error->stack) { scr_dyn_obj_set(out, "stack", 5, scr_dyn_new_str(error->stack)); - out->v.obj.entries[out->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(out)[out->v.obj.len - 1].enumerable = false; } scr_error_release(error); continue; @@ -14947,6 +15283,11 @@ static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_ continue; } if (current->kind != SCR_DYN_OBJ) continue; + if (current->v.obj.layout) { + ScrDyn **slot = scr_dyn_layout_slot(current, key, key_len); + if (slot) return scr_dyn_retain(*slot); + continue; + } ScrDynEntry *entry = scr_dyn_find_entry(current, key, key_len); if (entry) { if (!entry->accessor) return scr_dyn_retain(entry->value); @@ -15072,9 +15413,38 @@ static void scr_dyn_read_ics_reset(void) { scr_dyn_read_ics_registered = false; } +#ifndef SCR_RC_AUDIT +/* Points the site at `key` (shared key bytes). */ +static void scr_dyn_read_ic_key(ScrDynReadIc *ic, char *key, size_t key_len) { + if (ic->key == key) return; + scr_dyn_key_retain(key); + if (ic->key) { + scr_dyn_key_release(ic->key, ic->key_len); + } else { + if (!scr_dyn_read_ics_registered) { + scr_dyn_read_ics_registered = true; + scr_atexit(scr_dyn_read_ics_reset); + } + ic->next = scr_dyn_read_ics; + scr_dyn_read_ics = ic; + } + ic->key = key; + ic->key_len = key_len; +} +#endif + static __attribute__((noinline)) ScrDyn *scr_dyn_obj_read_ic_miss(const ScrDyn *d, const ScrStr *key, ScrDynReadIc *ic) { #ifndef SCR_RC_AUDIT + if (d->kind == SCR_DYN_OBJ && d->v.obj.layout) { + uint32_t i = d != scr_builtin_object_prototype + ? scr_layout_find(d->v.obj.layout, key->data, key->len) : UINT32_MAX; + if (i == UINT32_MAX) return scr_dyn_obj_read_receiver(d, key->data, key->len, d); + const ScrLayoutKey *k = &d->v.obj.layout->keys[i]; + scr_dyn_read_ic_key(ic, k->key, k->len); + ic->index = i; + return scr_dyn_retain(d->v.obj.slots[i]); + } ScrDynEntry *entry = d != scr_builtin_object_prototype ? scr_dyn_find_entry(d, key->data, key->len) : NULL; if (entry && !entry->accessor) { if (ic->key != entry->key) { @@ -15105,6 +15475,16 @@ static __attribute__((noinline)) ScrDyn *scr_dyn_obj_read_ic_miss(const ScrDyn * * do not, and compare their bytes at the remembered position instead. */ ScrDyn *scr_dyn_obj_read_ic(const ScrDyn *d, const ScrStr *key, ScrDynReadIc *ic) { size_t index = ic->index; + const ScrLayout *layout = d->kind == SCR_DYN_OBJ ? d->v.obj.layout : NULL; + if (layout) { + if (index < d->v.obj.len) { + const ScrLayoutKey *k = &layout->keys[index]; + if ((k->key == ic->key || (k->len == key->len && scr_key_equal(k->key, key->data, key->len))) && + d != scr_builtin_object_prototype) + return scr_dyn_retain(d->v.obj.slots[index]); + } + return scr_dyn_obj_read_ic_miss(d, key, ic); + } if (index < d->v.obj.len) { const ScrDynEntry *entry = &d->v.obj.entries[index]; if ((entry->key == ic->key || @@ -15301,7 +15681,8 @@ static size_t *scr_dyn_obj_key_order(const ScrDyn *v) { ScrKeyIndex *indices = NULL; size_t count = 0; for (size_t i = 0; i < n; i++) { - const ScrDynEntry *entry = &v->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(v, i); + const ScrDynProp *entry = &entry_prop; uint32_t index; if (!scr_key_array_index(entry->key, entry->key_len, &index)) continue; if (!indices) { @@ -15318,7 +15699,8 @@ static size_t *scr_dyn_obj_key_order(const ScrDyn *v) { for (size_t i = 0; i < count; i++) order[i] = indices[i].entry; free(indices); for (size_t i = 0; i < n; i++) { - const ScrDynEntry *entry = &v->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(v, i); + const ScrDynProp *entry = &entry_prop; uint32_t index; if (!scr_key_array_index(entry->key, entry->key_len, &index)) order[count++] = i; } @@ -15329,8 +15711,9 @@ static size_t *scr_dyn_obj_key_order(const ScrDyn *v) { * walk over its values runs no user code and the object cannot change * under it. */ static bool scr_dyn_obj_data_only(const ScrDyn *v) { + if (v->v.obj.layout) return true; for (size_t i = 0; i < v->v.obj.len; i++) - if (v->v.obj.entries[i].accessor) return false; + if (scr_dyn_obj_dict_entries(v)[i].accessor) return false; return true; } @@ -15339,7 +15722,8 @@ ScrDyn *scr_dyn_obj_own_keys(const ScrDyn *v) { ScrDyn *keys = scr_dyn_new_arr(); scr_dyn_arr_reserve(keys, v->v.obj.len); for (size_t i = 0; i < v->v.obj.len; i++) { - const ScrDynEntry *entry = &v->v.obj.entries[order ? order[i] : i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(v, order ? order[i] : i); + const ScrDynProp *entry = &entry_prop; scr_dyn_arr_push(keys, scr_dyn_objwalk_key(entry->key, entry->key_len)); } free(order); @@ -15552,7 +15936,8 @@ static ScrDyn *scr_dyn_obj_data_walk(const ScrDyn *v, ScrObjWalk mode) { ScrDyn *out = scr_dyn_new_arr(); scr_dyn_arr_reserve(out, v->v.obj.len); for (size_t i = 0; i < v->v.obj.len; i++) { - const ScrDynEntry *entry = &v->v.obj.entries[order ? order[i] : i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(v, order ? order[i] : i); + const ScrDynProp *entry = &entry_prop; if (entry->enumerable) scr_dyn_objwalk_push(out, mode, entry->key, entry->key_len, entry->value); } @@ -15577,7 +15962,8 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) { * the shared numeric/insertion ordering over a names-only table. */ ScrDyn *names = scr_dyn_new_obj(); for (size_t i = 0; i < v->v.proxy.target->v.obj.len; i++) { - const ScrDynEntry *entry = &v->v.proxy.target->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(v->v.proxy.target, i); + const ScrDynProp *entry = &entry_prop; scr_dyn_obj_set(names, entry->key, entry->key_len, scr_dyn_retain(scr_dyn_undefined())); } raw = scr_dyn_obj_keys(names); @@ -15619,7 +16005,8 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) { scr_dyn_release(raw); const ScrDyn *target = v->v.proxy.target; for (size_t i = 0; target->kind == SCR_DYN_OBJ && i < target->v.obj.len; i++) { - const ScrDynEntry *entry = &target->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(target, i); + const ScrDynProp *entry = &entry_prop; if (entry->configurable) continue; bool found = false; for (size_t j = 0; j < keys->v.arr.len; j++) { @@ -15888,7 +16275,8 @@ static ScrArr *scr_dyn_own_key_strs(const ScrDyn *v, bool shared) { size_t *order = scr_dyn_obj_key_order(v); ScrArr *out = scr_arr_new(SCR_ELEM_STR, v->v.obj.len); for (size_t i = 0; i < v->v.obj.len; i++) { - const ScrDynEntry *entry = &v->v.obj.entries[order ? order[i] : i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(v, order ? order[i] : i); + const ScrDynProp *entry = &entry_prop; if (entry->enumerable) scr_arr_push_ref(out, scr_str_new(entry->key, entry->key_len)); } free(order); @@ -15958,7 +16346,8 @@ ScrDynEntryPairs *scr_dyn_obj_entry_pairs(const ScrDyn *v) { size_t *order = scr_dyn_obj_key_order(v); ScrDynEntryPairs *out = scr_dyn_entry_pairs_new(v->v.obj.len); for (size_t i = 0; i < v->v.obj.len; i++) { - const ScrDynEntry *entry = &v->v.obj.entries[order ? order[i] : i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(v, order ? order[i] : i); + const ScrDynProp *entry = &entry_prop; if (!entry->enumerable) continue; out->pairs[out->count++] = (ScrDynEntryPair){scr_str_new(entry->key, entry->key_len), scr_dyn_retain(entry->value)}; @@ -16021,7 +16410,8 @@ ScrArr *scr_dyn_obj_value_list(const ScrDyn *v) { uint64_t *slots = scr_dyn_value_slots(v->v.obj.len, stack, 16); size_t count = 0; for (size_t i = 0; i < v->v.obj.len; i++) { - const ScrDynEntry *entry = &v->v.obj.entries[order ? order[i] : i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(v, order ? order[i] : i); + const ScrDynProp *entry = &entry_prop; if (entry->enumerable) slots[count++] = (uint64_t)(uintptr_t)scr_dyn_retain(entry->value); } free(order); @@ -16272,7 +16662,7 @@ static void scr_dyn_copy_property_descriptor(ScrDyn *target, const ScrDyn *src, ScrDyn *setter = accessor && from->setter ? scr_dyn_retain(from->setter) : NULL; ScrStr *name = slot ? scr_str_new(from->key, from->key_len) : NULL; scr_dyn_obj_set(target, from->key, from->key_len, value); - ScrDynEntry *entry = &target->v.obj.entries[target->v.obj.len - 1]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(target)[target->v.obj.len - 1]; entry->accessor = accessor; entry->getter = getter; entry->setter = setter; @@ -16295,7 +16685,7 @@ ScrDyn *scr_dyn_copy_property_descriptors(ScrDyn *target, const ScrDyn *src) { if (src->v.obj.len) scr_dyn_region_store(target, NULL); size_t *order = scr_dyn_obj_key_order(src); for (size_t i = 0; i < src->v.obj.len && !scr_exc_pending(); i++) - scr_dyn_copy_property_descriptor(target, src, &src->v.obj.entries[order ? order[i] : i]); + scr_dyn_copy_property_descriptor(target, src, &scr_dyn_obj_dict_entries(src)[order ? order[i] : i]); free(order); } else { ScrDyn *keys = scr_dyn_obj_own_keys(src); @@ -16627,9 +17017,9 @@ ScrError *scr_error_new_options(int kind, const ScrDyn *message, const ScrDyn *o static void scr_suppressed_attach(ScrError *e, ScrDyn *error, ScrDyn *suppressed) { ScrDyn *view = scr_dyn_from_error(e); scr_dyn_obj_set(view, "error", 5, error); - view->v.obj.entries[view->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(view)[view->v.obj.len - 1].enumerable = false; scr_dyn_obj_set(view, "suppressed", 10, suppressed); - view->v.obj.entries[view->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(view)[view->v.obj.len - 1].enumerable = false; scr_dyn_release(view); } @@ -17228,7 +17618,8 @@ static ScrDyn *scr_ffi_open(ScrDyn *catalog, const ScrDyn *path, const ScrDyn *d if (scr_ffi_text_equal(scr_dyn_obj_get(catalog->v.arr.items[i], "library", 7), path)) found = true; if (!found) goto invalid; for (size_t i = 0; i < definitions->v.obj.len; i++) { - const ScrDynEntry *requested = &definitions->v.obj.entries[i]; + const ScrDynProp requested_prop = scr_dyn_obj_prop_at(definitions, i); + const ScrDynProp *requested = &requested_prop; if (!requested->enumerable) continue; if (requested->accessor || requested->value->kind != SCR_DYN_OBJ) goto invalid; ScrDyn *entry = NULL; @@ -18438,7 +18829,7 @@ ScrDyn *scr_dyn_freeze_data(ScrDyn *value) { if (value->kind == SCR_DYN_OBJ && !value->copied_from_native) { value->non_extensible = true; for (size_t i = 0; i < value->v.obj.len; i++) { - ScrDynEntry *entry = &value->v.obj.entries[i]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(value)[i]; entry->configurable = false; if (!entry->accessor) entry->writable = false; } @@ -18474,7 +18865,8 @@ bool scr_dyn_is_frozen(const ScrDyn *value) { if (value->kind == SCR_DYN_OBJ && !value->copied_from_native) { if (!value->non_extensible) return false; for (size_t i = 0; i < value->v.obj.len; i++) { - const ScrDynEntry *entry = &value->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(value, i); + const ScrDynProp *entry = &entry_prop; if (entry->configurable || (!entry->accessor && entry->writable)) return false; } return true; @@ -18536,7 +18928,7 @@ void scr_dyn_class_field_attributes(ScrDyn *view, ScrStr *name, double bits) { static void scr_dyn_class_table_attributes(ScrDyn *table, unsigned b) { table->non_extensible = true; for (size_t i = 0; i < table->v.obj.len; i++) { - ScrDynEntry *entry = &table->v.obj.entries[i]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(table)[i]; if (b & (SCR_CLASS_SEALED | SCR_CLASS_FROZEN)) entry->configurable = false; if ((b & SCR_CLASS_FROZEN) && !entry->accessor) entry->writable = false; } @@ -18757,12 +19149,8 @@ bool scr_dyn_iterates_by_protocol(const ScrDyn *value) { const ScrDyn *scr_dyn_tier_object_prototype(void) { return scr_builtin_object_prototype; } -char *scr_dyn_tier_key_copy(const char *key, size_t key_len) { return scr_dyn_key_copy(key, key_len); } - -void scr_dyn_tier_key_release(char *key, size_t key_len) { scr_dyn_key_release(key, key_len); } - -void scr_dyn_tier_append(ScrDyn *obj, char *key, size_t key_len, ScrDyn *value) { - scr_dyn_obj_append_shared(obj, key, key_len, value); +void scr_dyn_tier_append(ScrDyn *obj, const ScrLayout *next, ScrDyn *value) { + scr_dyn_obj_append_shared(obj, next, value); } #endif diff --git a/packages/runtime/src/scr_json.test.c b/packages/runtime/src/scr_json.test.c index 29ba4bfe3..4ecae73b5 100644 --- a/packages/runtime/src/scr_json.test.c +++ b/packages/runtime/src/scr_json.test.c @@ -10,6 +10,7 @@ #ifdef SCR_RC_AUDIT long scr_str_live_count(void); long scr_dyn_key_live_count(void); +void scr_layout_reset_idle(void); #endif static void put(ScrDyn *map, ScrDyn *key, ScrDyn *value) { @@ -167,7 +168,7 @@ static void checked_storage(void) { char key[16]; int length = snprintf(key, sizeof key, "key-%d", i); scr_dyn_obj_set(object, key, (size_t)length, scr_dyn_new_num(i)); - assert((object->v.obj.index != NULL) == (i >= 31)); + assert((object->v.obj.index != NULL) == (i >= (int)SCR_LAYOUT_MAX_KEYS)); } for (int i = 0; i < 129; i++) { char key[16]; @@ -229,6 +230,8 @@ static void checked_storage(void) { static void shared_property_keys(void) { #ifdef SCR_RC_AUDIT + /* Layouts hold key references until the thread's state is torn down. */ + scr_layout_reset_idle(); long before = scr_dyn_key_live_count(); #endif /* Distinct objects share bytes, not values or property attributes. Key @@ -238,7 +241,8 @@ static void shared_property_keys(void) { 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); + size_t first_len, second_len; + assert(scr_dyn_obj_key_at(first, 0, &first_len) == scr_dyn_obj_key_at(second, 0, &second_len)); key[0] = 'x'; ScrStr *name = scr_str_new("shared\0name", sizeof key - 1); scr_dyn_key_delete(first, name, true); @@ -269,6 +273,7 @@ static void shared_property_keys(void) { } } #ifdef SCR_RC_AUDIT + scr_layout_reset_idle(); assert(scr_dyn_key_live_count() == before); #endif } @@ -651,8 +656,10 @@ static void json_schema_parse(void) { static const char *const order[] = {"tags", "ok", "extra", "qty", "sku"}; assert(view->kind == SCR_DYN_OBJ && view->v.obj.len == 5); for (size_t i = 0; i < 5; i++) { - assert(view->v.obj.entries[i].key_len == strlen(order[i])); - assert(memcmp(view->v.obj.entries[i].key, order[i], strlen(order[i])) == 0); + size_t key_len; + const char *key = scr_dyn_obj_key_at(view, i, &key_len); + assert(key_len == strlen(order[i])); + assert(memcmp(key, order[i], strlen(order[i])) == 0); } assert(scr_dyn_obj_get(view, "extra", 5)->kind == SCR_DYN_OBJ); test_item_release(second); diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c index 419dcea4b..382907219 100644 --- a/packages/runtime/src/scr_message.c +++ b/packages/runtime/src/scr_message.c @@ -702,9 +702,10 @@ static ScrMessage *scr_message_try_publish(const ScrDyn *value) { } else if (d->rc != SIZE_MAX) { scr_cyc_on_dead(d); /* leave the candidate and tenured buffers */ d->rc = SIZE_MAX; - /* A shape names a node of this thread's transition tree (scr_js.h), - * which other threads cannot use and which ends with this thread. */ - if (d->kind == SCR_DYN_OBJ) d->v.obj.shape = NULL; + /* A layout names a node of this thread's transition tree + * (scr_dyn.h), which other threads cannot use and which ends with + * this thread: the object moves to dictionary mode. */ + if (d->kind == SCR_DYN_OBJ) scr_dyn_to_dictionary(d); #ifdef SCR_RC_AUDIT scr_dyn_live_forget(); #endif diff --git a/packages/runtime/src/scr_message.test.c b/packages/runtime/src/scr_message.test.c index b6adf34c5..10459255b 100644 --- a/packages/runtime/src/scr_message.test.c +++ b/packages/runtime/src/scr_message.test.c @@ -225,8 +225,11 @@ static void check_large(void) { ScrDyn *decoded = scr_message_decode(message); assert(decoded && !scr_exc_pending()); assert(decoded->v.obj.len == 42); - assert(decoded->v.obj.entries[0].key_len == 5 && !memcmp(decoded->v.obj.entries[0].key, "item0", 5)); - assert(decoded->v.obj.entries[41].key_len == 6 && !memcmp(decoded->v.obj.entries[41].key, "nested", 6)); + size_t member_len; + const char *member = scr_dyn_obj_key_at(decoded, 0, &member_len); + assert(member_len == 5 && !memcmp(member, "item0", 5)); + member = scr_dyn_obj_key_at(decoded, 41, &member_len); + assert(member_len == 6 && !memcmp(member, "nested", 6)); assert(scr_dyn_obj_get(decoded, "nested", 6)->v.num == 42); ScrDyn *first = scr_dyn_obj_get(decoded, "item0", 5); for (int i = 0; i < 40; i++) { diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index 1ba2b22cd..1b29a63aa 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -481,9 +481,9 @@ void scr_publish(void *root, ScrPubFn fn) { void *obj = scr_pub_log_obj(&ctx.log[i]); if (ctx.log[i].obj & SCR_PUB_DYN) { ScrDyn *d = obj; - /* Other threads share the object; its shape belongs to this one - * (scr_js.h). */ - if (d->kind == SCR_DYN_OBJ) d->v.obj.shape = NULL; + /* Other threads share the object; layouts belong to this thread + * (scr_dyn.h), so it moves to dictionary mode. */ + if (d->kind == SCR_DYN_OBJ) scr_dyn_to_dictionary(d); scr_dyn_release(scr_dyn_freeze_data(d)); } #ifdef SCR_RC_AUDIT diff --git a/packages/runtime/test/object-core.test.ts b/packages/runtime/test/object-core.test.ts index 04f7cfb38..ce9e1e3e0 100644 --- a/packages/runtime/test/object-core.test.ts +++ b/packages/runtime/test/object-core.test.ts @@ -10,13 +10,14 @@ const src = join(import.meta.dirname, "..", "src"); * change how it stores them. */ const CORE = new Set(["scr_json.c", "scr_js_object.c", "scr_dyn.h"]); -test("only the object core reads ordinary objects' entry storage", () => { +test("only the object core reads ordinary objects' storage", () => { const offenders: string[] = []; for (const name of readdirSync(src)) { if (!/\.(c|h)$/.test(name) || name.endsWith(".test.c") || CORE.has(name)) continue; const lines = readFileSync(join(src, name), "utf8").split("\n"); lines.forEach((line, i) => { - if (/\bv\.obj\.(entries|len)\b/.test(line)) offenders.push(`${name}:${i + 1}: ${line.trim()}`); + if (/\bv\.obj\.(entries|slots|layout|len|cap|index)\b/.test(line)) + offenders.push(`${name}:${i + 1}: ${line.trim()}`); }); } expect(offenders).toEqual([]); diff --git a/packages/runtime/test/test_json.c b/packages/runtime/test/test_json.c index 865f1c757..6d492065d 100644 --- a/packages/runtime/test/test_json.c +++ b/packages/runtime/test/test_json.c @@ -571,10 +571,10 @@ static ScrDyn *define_from(ScrDyn *target, const char *key, ScrDyn *value, bool } static const ScrDynEntry *entry_of(const ScrDyn *obj, const char *key) { + const ScrDynEntry *entries = scr_dyn_obj_dict_entries(obj); for (size_t i = 0; i < obj->v.obj.len; i++) - if (obj->v.obj.entries[i].key_len == strlen(key) && - memcmp(obj->v.obj.entries[i].key, key, strlen(key)) == 0) - return &obj->v.obj.entries[i]; + if (entries[i].key_len == strlen(key) && memcmp(entries[i].key, key, strlen(key)) == 0) + return &entries[i]; return NULL; } @@ -618,7 +618,7 @@ static void test_copy_property_descriptors(void) { scr_dyn_obj_set(existing, "label", 5, scr_dyn_new_num(9)); scr_dyn_release(scr_dyn_copy_property_descriptors(existing, bag)); const ScrDynEntry *label = entry_of(existing, "label"); - check(!scr_exc_pending() && existing->v.obj.len == 5 && existing->v.obj.entries[0].key_len == 5 && + check(!scr_exc_pending() && existing->v.obj.len == 5 && scr_dyn_obj_dict_entries(existing)[0].key_len == 5 && label->value->v.num == 1, "copy redefines an existing property in place");