Copy class-view property bags entry by entry

Materializing the checked view of a JavaScript class instance copies its
property bag through scr_dyn_copy_property_descriptors, which built a key
string and a descriptor object per property and defined each through
scr_dyn_define_property (about 2k instructions per property). Plain objects
on both sides now copy each entry directly with the attributes, value or
accessor pair and own-key order the definition gives; properties the target
already has, non-extensible targets and other receivers keep the general
path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-23 00:29:22 -07:00
co-authored by Claude Opus 5.5
parent 80f0bfada2
commit 342cf3a063
5 changed files with 248 additions and 9 deletions
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7620-js-class-view-descriptors.js"
],
"diags": []
}
+64 -9
View File
@@ -13699,19 +13699,74 @@ ScrDyn *scr_dyn_object_rest(const ScrDyn *source, const ScrDyn *excluded) {
/* Compiler-owned native class views retain every property and its flags,
* including nonenumerable data, without invoking accessors. */
ScrDyn *scr_dyn_copy_property_descriptors(ScrDyn *target, const ScrDyn *src) {
if (!src || src->kind == SCR_DYN_UNDEF) return scr_dyn_retain(target);
ScrDyn *keys = scr_dyn_obj_own_keys(src);
if (!keys) return NULL;
for (size_t i = 0; i < keys->v.arr.len; i++) {
ScrDyn *key = keys->v.arr.items[i];
ScrDyn *descriptor = scr_dyn_own_descriptor(src, key->v.str);
static void scr_dyn_copy_property_descriptor(ScrDyn *target, const ScrDyn *src,
const ScrDynEntry *from) {
/* Exactly scr_dyn_define_property(target, key, own descriptor of key)
* when `target` has no such property and accepts new ones: the entry
* takes the source's attributes, value or accessor pair. */
ScrDynEntry *prior = scr_dyn_find_entry(target, from->key, from->key_len);
if (prior || target->non_extensible) {
ScrStr *name = scr_str_new(from->key, from->key_len);
ScrDyn *key = scr_dyn_new_str(name);
ScrDyn *descriptor = scr_dyn_own_descriptor(src, name);
ScrDyn *result = descriptor ? scr_dyn_define_property(target, key, descriptor) : NULL;
scr_dyn_release(descriptor);
scr_dyn_release(result);
if (scr_exc_pending()) break;
scr_dyn_release(key);
scr_str_release(name);
return;
}
/* The names whose definition may move into a native Error's slots
* (scr_error_sync_slots). */
static const char *const slots[] = {"cause", "stack", "name", "message", "code"};
bool slot = false;
for (size_t i = 0; i < sizeof slots / sizeof *slots && !slot; i++)
slot = from->key_len == strlen(slots[i]) && memcmp(from->key, slots[i], from->key_len) == 0;
bool accessor = from->accessor, writable = !accessor && from->writable;
bool enumerable = from->enumerable, configurable = from->configurable;
ScrDyn *value = scr_dyn_retain(accessor ? scr_dyn_undefined() : from->value);
ScrDyn *getter = accessor && from->getter ? scr_dyn_retain(from->getter) : NULL;
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];
entry->accessor = accessor;
entry->getter = getter;
entry->setter = setter;
entry->writable = writable;
entry->enumerable = enumerable;
entry->configurable = configurable;
if (name) {
scr_error_sync_cause(target, name);
scr_str_release(name);
}
}
ScrDyn *scr_dyn_copy_property_descriptors(ScrDyn *target, const ScrDyn *src) {
if (!src || src->kind == SCR_DYN_UNDEF) return scr_dyn_retain(target);
if (src->kind == SCR_DYN_OBJ && target->kind == SCR_DYN_OBJ && src != target &&
src != scr_builtin_object_prototype && target != scr_builtin_error_prototype) {
/* Plain objects on both sides: no fence, proxy trap or accessor runs,
* so each property is copied from its entry without building the key
* and descriptor objects the general path goes through. */
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]);
free(order);
} else {
ScrDyn *keys = scr_dyn_obj_own_keys(src);
if (!keys) return NULL;
for (size_t i = 0; i < keys->v.arr.len; i++) {
ScrDyn *key = keys->v.arr.items[i];
ScrDyn *descriptor = scr_dyn_own_descriptor(src, key->v.str);
ScrDyn *result = descriptor ? scr_dyn_define_property(target, key, descriptor) : NULL;
scr_dyn_release(descriptor);
scr_dyn_release(result);
if (scr_exc_pending()) break;
}
scr_dyn_release(keys);
}
scr_dyn_release(keys);
if (!scr_exc_pending()) {
ScrDyn *symbols = scr_dyn_get_own_property_symbols(src);
if (!symbols) return NULL;
+86
View File
@@ -551,6 +551,91 @@ static void test_class_values(void) {
scr_str_release(name);
}
/* The class-view copy (scr_dyn_copy_property_descriptors) gives each
* property of the bag the attributes, value or accessors, and own-key
* position that defining it from its own descriptor would. */
static ScrDyn *define_from(ScrDyn *target, const char *key, ScrDyn *value, bool writable,
bool enumerable, bool configurable) {
ScrDyn *descriptor = scr_dyn_new_obj();
scr_dyn_obj_set(descriptor, "value", 5, value);
scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool(writable));
scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(enumerable));
scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(configurable));
ScrStr *name = S(key);
ScrDyn *k = scr_dyn_new_str(name);
ScrDyn *result = scr_dyn_define_property(target, k, descriptor);
scr_dyn_release(k);
scr_str_release(name);
scr_dyn_release(descriptor);
return result;
}
static const ScrDynEntry *entry_of(const ScrDyn *obj, const char *key) {
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];
return NULL;
}
static void test_copy_property_descriptors(void) {
ScrDyn *bag = scr_dyn_new_obj();
ScrDyn *getter = scr_dyn_new_obj();
scr_dyn_release(define_from(bag, "label", scr_dyn_new_num(1), true, true, true));
scr_dyn_release(define_from(bag, "hidden", scr_dyn_new_num(2), false, false, true));
scr_dyn_obj_set(bag, "7", 1, scr_dyn_new_num(7));
scr_dyn_obj_set(bag, "3", 1, scr_dyn_new_num(3));
/* An accessor entry (the getter object stands in for its function). */
scr_dyn_obj_set(bag, "computed", 8, scr_dyn_retain(scr_dyn_undefined()));
ScrDynEntry *accessor = (ScrDynEntry *)entry_of(bag, "computed");
accessor->accessor = true;
accessor->getter = scr_dyn_retain(getter);
accessor->writable = false;
accessor->enumerable = false;
accessor->configurable = false;
ScrDyn *view = scr_dyn_new_obj();
scr_dyn_release(scr_dyn_copy_property_descriptors(view, bag));
check(!scr_exc_pending() && view->v.obj.len == 5, "copy keeps every bag property");
ScrDyn *keys = scr_dyn_obj_own_keys(view);
const char *order[] = {"3", "7", "label", "hidden", "computed"};
bool ordered = keys->v.arr.len == 5;
for (size_t i = 0; ordered && i < 5; i++) ordered = str_is(keys->v.arr.items[i]->v.str, order[i]);
check(ordered, "copy keeps the bag's own-key order");
scr_dyn_release(keys);
const ScrDynEntry *hidden = entry_of(view, "hidden");
check(hidden && !hidden->accessor && hidden->value->v.num == 2 && !hidden->writable &&
!hidden->enumerable && hidden->configurable,
"copy keeps data attributes");
const ScrDynEntry *copied = entry_of(view, "computed");
check(copied && copied->accessor && copied->getter == getter && !copied->setter &&
!copied->writable && !copied->enumerable && !copied->configurable &&
copied->value->kind == SCR_DYN_UNDEF,
"copy keeps an accessor without calling it");
/* A property the target already has is redefined like defineProperty. */
ScrDyn *existing = scr_dyn_new_obj();
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 &&
label->value->v.num == 1,
"copy redefines an existing property in place");
/* A target that takes no new properties throws as defineProperty does. */
ScrDyn *closed = scr_dyn_new_obj();
scr_dyn_release(scr_dyn_prevent_extensions(closed));
ScrDyn *refused = scr_dyn_copy_property_descriptors(closed, bag);
check(!refused && scr_exc_pending() && closed->v.obj.len == 0, "copy onto a non-extensible object throws");
scr_exc_clear();
scr_dyn_release(closed);
scr_dyn_release(existing);
scr_dyn_release(view);
scr_dyn_release(bag);
scr_dyn_release(getter);
}
static void test_primitive_owners(void) {
ScrDyn *values[] = {scr_dyn_new_null(), scr_dyn_new_bool(false), scr_dyn_new_bool(true)};
const char *texts[] = {"null", "false", "true"};
@@ -579,6 +664,7 @@ int main(void) {
test_class_values();
scr_init();
test_primitive_owners();
test_copy_property_descriptors();
/* ── primitives ─────────────────────────────────────────────────── */
ScrDyn *d = parse_ok("null", "parse null");
@@ -0,0 +1,91 @@
// An instance of a JS class with public fields keeps its own properties in
// a property bag, and untyped code that calls a method on it sees a view
// built from that bag: data properties with their attributes, accessors,
// index-like keys (which come first, in ascending order), symbols, and
// properties added or removed after construction. Every refresh of the view
// must copy the bag exactly.
'use strict';
class Box {
label = 'box';
count = 0;
constructor(n) {
this.count = n;
}
describe() {
return `${this.label}:${this.count}`;
}
bump() {
this.count++;
return this;
}
}
function call(target, method, ...args) {
return target[method](...args);
}
function put(target, key, value) {
target[key] = value;
}
function drop(target, key) {
delete target[key];
}
function show(title, target) {
const lines = [];
for (const key of Reflect.ownKeys(target)) {
const d = Object.getOwnPropertyDescriptor(target, key);
const kind = 'value' in d ? `value=${String(d.value)} writable=${d.writable}` : `get=${typeof d.get} set=${typeof d.set}`;
lines.push(` ${String(key)}: ${kind} enumerable=${d.enumerable} configurable=${d.configurable}`);
}
console.log(`${title}\n${lines.join('\n')}`);
}
const box = new Box(2);
Object.defineProperty(box, 'hidden', { value: 42, enumerable: false, writable: false, configurable: true });
Object.defineProperty(box, 'computed', {
get() {
return this.count * 10;
},
set(v) {
this.count = v / 10;
},
enumerable: true,
configurable: true,
});
Object.defineProperty(box, 'getterOnly', { get: () => 'g', enumerable: false, configurable: false });
put(box, 2, 'two');
put(box, 1, 'one');
put(box, 'extra', 'x');
const tag = Symbol('tag');
put(box, tag, 'symbol');
console.log(call(box, 'describe'));
show('after construction', box);
console.log('keys', Object.keys(box).join(','));
call(box, 'bump');
put(box, 'computed', 70);
drop(box, 'extra');
put(box, 'later', true);
console.log(call(box, 'describe'));
show('after changes', box);
console.log('computed', box.computed, 'hidden', box.hidden, 'getterOnly', box.getterOnly);
// Many views in a row see the same, current properties.
let total = 0;
for (let i = 0; i < 5; i++) {
call(box, 'bump');
total += Object.keys(box).length;
}
console.log('total keys', total, call(box, 'describe'));
const other = new Box(7);
console.log(call(other, 'describe'), Object.keys(other).join(','));
try {
call(box, 'missing');
} catch (error) {
console.log(error instanceof TypeError, error.message);
}
@@ -0,0 +1 @@
{ "plain": 3000, "san": 4500 }