From a77fe8c21c67372b3a1af4ad69fcad34a578ad59 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 09:17:32 -0700 Subject: [PATCH] Decide an untyped property write with one own-property lookup scr_dyn_object_key_set, behind every untyped property write and every member of an untyped object literal, looked the key up as many as five times: a linear scan for an own accessor or read-only entry, a lookup before the prototype walk, one before the non-extensible check, one to know whether the key was created, and one more inside the store. The own property is now looked up once. An own data property is written in place (an own setter still runs; a read-only or getter-only property still refuses), and an absent key walks the prototype chain as before and is then added by scr_dyn_obj_set, which is unchanged. zod 3, z.array(Order).parse over 500 orders: 199.8M -> 194.1M instructions per round (-2.8%). Co-Authored-By: Claude Opus 5.5 --- .../corpus/7793-untyped-key-writes.js.json | 6 ++ packages/runtime/src/scr_json.c | 21 +++-- tests/corpus/7793-untyped-key-writes.js | 83 +++++++++++++++++++ .../7793-untyped-key-writes.js.json | 1 + 4 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7793-untyped-key-writes.js.json create mode 100644 tests/corpus/7793-untyped-key-writes.js create mode 100644 tests/harness/corpus-costs/7793-untyped-key-writes.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7793-untyped-key-writes.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7793-untyped-key-writes.js.json new file mode 100644 index 000000000..b887e0fdb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7793-untyped-key-writes.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7793-untyped-key-writes.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 14d0a901f..a1ab99a42 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -9369,10 +9369,12 @@ bool scr_dyn_in(const ScrDyn *v, const ScrStr *key) { return false; } +/* One lookup of the own property decides the assignment: an own property + * 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) { - for (size_t i = 0; i < recv->v.obj.len; i++) { - ScrDynEntry *entry = &recv->v.obj.entries[i]; - if (entry->key_len != key->len || memcmp(entry->key, key->data, key->len) != 0) continue; + ScrDynEntry *entry = scr_dyn_entry(recv, key); + if (entry) { if (entry->accessor && entry->setter) { ScrDyn *args[] = {value}; ScrDyn *setter = scr_dyn_retain(entry->setter); @@ -9388,8 +9390,14 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); return; } + scr_dyn_region_store(recv, value); + ScrDyn *old = entry->value; + entry->value = scr_dyn_retain(value); + scr_dyn_release(old); + scr_error_sync_cause(recv, key); + return; } - if (!scr_dyn_entry(recv, key)) { + { for (const ScrDyn *current = scr_dyn_proto_next(recv); current;) { ScrDyn *table = current->kind == SCR_DYN_FUNC ? scr_dyn_fn_properties(current) : NULL; const ScrDyn *object = table ? table : current; @@ -9421,7 +9429,7 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con break; } } - if (recv->non_extensible && !scr_dyn_entry(recv, key)) { + if (recv->non_extensible) { ScrJsonBuf buffer; scr_jb_init(&buffer); scr_jb_puts(&buffer, "Cannot add property "); @@ -9430,9 +9438,8 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); return; } - bool created = scr_dyn_entry(recv, key) == NULL; scr_dyn_obj_set(recv, key->data, key->len, scr_dyn_retain(value)); - if (created && key->len == 5 && memcmp(key->data, "stack", 5) == 0 && + if (key->len == 5 && memcmp(key->data, "stack", 5) == 0 && scr_errdyn_find(recv, false) != SIZE_MAX) { /* An error's own `stack` exists from construction as a non-enumerable * data property, so assigning it adds no enumerable key. */ diff --git a/tests/corpus/7793-untyped-key-writes.js b/tests/corpus/7793-untyped-key-writes.js new file mode 100644 index 000000000..93158145e --- /dev/null +++ b/tests/corpus/7793-untyped-key-writes.js @@ -0,0 +1,83 @@ +"use strict"; +// Untyped property writes through one site: new keys append in order, +// existing keys keep their position, own and inherited setters run, +// getter-only and read-only properties refuse the write, and +// non-extensible objects refuse new keys, as in Node. + +/** @param {any} target @param {string} key @param {unknown} value */ +function put(target, key, value) { + target[key] = value; +} + +/** @param {() => void} fn */ +function attempt(label, fn) { + try { + fn(); + console.log(label, "ok"); + } catch (e) { + console.log(label, e instanceof TypeError ? "TypeError" : String(e)); + } +} + +const order = {}; +for (const key of ["b", "a", "c", "a", "b", "d"]) put(order, key, key.charCodeAt(0)); +console.log(JSON.stringify(order), Object.keys(order).join("")); + +const log = []; +const withSetter = { + _v: 0, + get v() { + return this._v; + }, + set v(value) { + log.push(`own ${value}`); + this._v = value * 2; + }, + get onlyGetter() { + return "g"; + }, +}; +put(withSetter, "v", 4); +console.log(withSetter.v, JSON.stringify(log)); +attempt("getter-only", () => put(withSetter, "onlyGetter", 1)); + +const proto = { + /** @param {number} value */ + set inherited(value) { + log.push(`inherited ${value} on ${this === child}`); + }, +}; +Object.defineProperty(proto, "fixed", { value: 1, writable: false }); +const child = Object.create(proto); +put(child, "inherited", 7); +attempt("inherited read-only", () => put(child, "fixed", 2)); +put(child, "own", 3); +console.log(JSON.stringify(log), Object.keys(child).join(","), child.fixed); + +/** @type {any} */ +const sealed = { x: 1 }; +Object.seal(sealed); +put(sealed, "x", 2); +attempt("sealed add", () => put(sealed, "y", 3)); +/** @type {any} */ +const closed = { x: 1 }; +Object.preventExtensions(closed); +put(closed, "x", 5); +attempt("closed add", () => put(closed, "z", 1)); +const frozen = Object.freeze({ x: 1 }); +attempt("frozen write", () => put(frozen, "x", 9)); +console.log(JSON.stringify(sealed), JSON.stringify(closed), JSON.stringify(frozen)); + +const readOnly = {}; +Object.defineProperty(readOnly, "k", { value: "kept", writable: false, enumerable: true }); +attempt("own read-only", () => put(readOnly, "k", "changed")); +console.log(readOnly.k); + +const error = new Error("boom"); +put(error, "cause", "why"); +put(error, "code", "E_X"); +console.log(error.cause, error.code); + +const wide = {}; +for (let i = 0; i < 40; i++) put(wide, `k${i % 25}`, i); +console.log(Object.keys(wide).length, wide.k0, wide.k24, Object.keys(wide).slice(0, 3).join(",")); diff --git a/tests/harness/corpus-costs/7793-untyped-key-writes.js.json b/tests/harness/corpus-costs/7793-untyped-key-writes.js.json new file mode 100644 index 000000000..eb1b1f127 --- /dev/null +++ b/tests/harness/corpus-costs/7793-untyped-key-writes.js.json @@ -0,0 +1 @@ +{ "plain": 4300, "san": 4800 }