Decide an untyped property write with one own-property lookup (#2014)

This commit is contained in:
Chris Tate
2026-10-23 12:04:52 -05:00
committed by GitHub
4 changed files with 104 additions and 7 deletions
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7793-untyped-key-writes.js"
],
"diags": []
}
+14 -7
View File
@@ -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. */
+83
View File
@@ -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(","));
@@ -0,0 +1 @@
{ "plain": 4300, "san": 4800 }