mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
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:
co-authored by
Claude Opus 5.5
parent
80f0bfada2
commit
342cf3a063
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7620-js-class-view-descriptors.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user