diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 417d88006..4387c13f9 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -32,10 +32,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 }. @@ -849,27 +848,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 }; } @@ -1441,9 +1436,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"); @@ -2777,9 +2771,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 3ec689de0..1b29c6b47 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -384,7 +384,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_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 98d4aef0b..1572c8a11 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -514,9 +514,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. */ @@ -524,7 +531,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 @@ -533,19 +540,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 @@ -624,10 +625,155 @@ _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 ────────────────────────────── + * 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 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; + 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 __attribute__((always_inline)) size_t scr_dyn_obj_count(const ScrDyn *o) { + return o->v.obj.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 __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 __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 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; + } + 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) { + 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; +} + /* ── 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 @@ -792,6 +938,27 @@ 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) { + 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, * scr_dyn_symbol_init_own gives an object that has no symbol properties * yet its first one, a writable, enumerable, configurable data property @@ -2027,7 +2194,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. */ @@ -2035,12 +2202,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_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_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 d0526f538..7b0d44069 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 & SCR_CLOSURE_KIND_OWN_PROTOTYPE) { 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); @@ -12553,6 +12900,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); @@ -12616,6 +12970,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; @@ -12649,13 +13004,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; } @@ -12667,41 +13023,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) { @@ -12776,14 +13109,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; } @@ -13594,7 +13927,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(); @@ -13603,12 +13936,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); @@ -13983,7 +14315,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)) || @@ -14006,7 +14339,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); @@ -14342,7 +14676,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) || @@ -14873,14 +15208,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; @@ -14993,6 +15329,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); @@ -15118,9 +15459,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) { @@ -15151,6 +15521,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 || @@ -15347,7 +15727,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) { @@ -15364,7 +15745,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; } @@ -15375,8 +15757,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; } @@ -15385,7 +15768,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); @@ -15598,7 +15982,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); } @@ -15623,7 +16008,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); @@ -15665,7 +16051,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++) { @@ -15934,7 +16321,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); @@ -16004,7 +16392,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)}; @@ -16067,7 +16456,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); @@ -16318,7 +16708,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; @@ -16341,7 +16731,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); @@ -16673,9 +17063,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); } @@ -17274,7 +17664,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; @@ -18484,7 +18875,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; } @@ -18520,7 +18911,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; @@ -18582,7 +18974,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; } @@ -18803,12 +19195,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_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..382907219 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; @@ -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 @@ -1033,14 +1034,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_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_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..1b29a63aa 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; @@ -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/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..ce9e1e3e0 --- /dev/null +++ b/packages/runtime/test/object-core.test.ts @@ -0,0 +1,24 @@ +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' 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|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");