Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions changelog.d/11015-integrity-shape-transition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
`Object.preventExtensions`, `Object.seal`, and `Object.freeze` now publish a new
ShapeId when they change an ordinary object's integrity flags. Previously,
`preventExtensions` left the shape unchanged, so a future shape-keyed property
add cache could reuse an edge learned while the object was extensible. Seal and
freeze only changed the shape indirectly when they updated an existing key's
descriptor, leaving keyless objects with the same gap.

Repeated calls with no new flag changes preserve the current ShapeId. A runtime
test covers all three operations on keyless objects and verifies that sibling
objects retain their original shape.
37 changes: 27 additions & 10 deletions crates/perry-runtime/src/object/object_ops_frozen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,18 @@ unsafe fn integrity_flags_are_writable(obj: *const ObjectHeader) -> bool {
!obj.is_null() && crate::value::addr_class::try_read_tracked_gc_header(obj as usize).is_some()
}

/// Extensibility is part of a shaped object's semantics. Retire its previous
/// ShapeId when an integrity flag changes so a shape-keyed add path cannot
/// reuse an edge learned while the object was extensible.
unsafe fn set_integrity_flags(obj: *mut ObjectHeader, flags: u16) {
let gc = gc_header_for(obj);
let added = (*gc)._reserved & flags != flags;
(*gc)._reserved |= flags;
if added {
shapes::transition_object_shape_semantics(obj);
}
}

#[no_mangle]
pub extern "C" fn js_object_freeze(obj_value: f64) -> f64 {
crate::array::subclass_elements::deopt_value(obj_value);
Expand All @@ -167,10 +179,12 @@ pub extern "C" fn js_object_freeze(obj_value: f64) -> f64 {
// no-op-and-return-the-value behaviour for a rejected receiver is
// unchanged (`test_gap_handle_band_object_ops` `Object.freeze(blob)`).
if integrity_flags_are_writable(obj) {
let gc = gc_header_for(obj);
(*gc)._reserved |= crate::gc::OBJ_FLAG_FROZEN
| crate::gc::OBJ_FLAG_SEALED
| crate::gc::OBJ_FLAG_NO_EXTEND;
set_integrity_flags(
obj,
crate::gc::OBJ_FLAG_FROZEN
| crate::gc::OBJ_FLAG_SEALED
| crate::gc::OBJ_FLAG_NO_EXTEND,
);
// TypedArray receivers are NOT `ObjectHeader`s — the key walk
// below would read a garbage `keys_array` off the TA header and
// can fault depending on heap layout. The GC flags above are the
Expand Down Expand Up @@ -277,17 +291,21 @@ pub extern "C" fn js_object_seal(obj_value: f64) -> f64 {
unsafe {
let obj = extract_obj_ptr(obj_value);
if integrity_flags_are_writable(obj) {
let gc = gc_header_for(obj);
(*gc)._reserved |= crate::gc::OBJ_FLAG_SEALED | crate::gc::OBJ_FLAG_NO_EXTEND;
set_integrity_flags(
obj,
crate::gc::OBJ_FLAG_SEALED | crate::gc::OBJ_FLAG_NO_EXTEND,
);
}
}
return obj_value;
}
unsafe {
let obj = extract_obj_ptr(obj_value);
if integrity_flags_are_writable(obj) {
let gc = gc_header_for(obj);
(*gc)._reserved |= crate::gc::OBJ_FLAG_SEALED | crate::gc::OBJ_FLAG_NO_EXTEND;
set_integrity_flags(
obj,
crate::gc::OBJ_FLAG_SEALED | crate::gc::OBJ_FLAG_NO_EXTEND,
);
// TypedArray receivers: GC flags only — see `js_object_freeze`.
if crate::typedarray::lookup_typed_array_kind(obj as usize).is_some()
|| crate::typedarray_props::typed_array_addr_from_value(obj_value).is_some()
Expand Down Expand Up @@ -389,8 +407,7 @@ pub extern "C" fn js_object_prevent_extensions(obj_value: f64) -> f64 {
crate::typedarray_props::typed_array_mark_no_extend(owner);
return obj_value;
}
let gc = gc_header_for(obj);
(*gc)._reserved |= crate::gc::OBJ_FLAG_NO_EXTEND;
set_integrity_flags(obj, crate::gc::OBJ_FLAG_NO_EXTEND);
}
}
obj_value
Expand Down
37 changes: 37 additions & 0 deletions crates/perry-runtime/src/object/shape_rules_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,43 @@ fn accessor() -> AccessorDescriptor {
const DEFAULT_ATTRS: PropertyAttrs = PropertyAttrs::new(true, true, true);
const FROZEN_ATTRS: PropertyAttrs = PropertyAttrs::new(false, true, false);

/// Keyless receivers expose the flag transition directly: no descriptor
/// install can incidentally mint a successor shape for these operations.
#[test]
fn rule1_integrity_flags_transition_keyless_shapes() {
let _lock = crate::gc::global_side_table_test_lock();
for (name, operation) in [
(
"preventExtensions",
super::js_object_prevent_extensions as extern "C" fn(f64) -> f64,
),
("seal", super::js_object_seal),
("freeze", super::js_object_freeze),
] {
unsafe {
let obj = shaped_object(&[]);
let sibling = shaped_object(&[]);
let before = shapes::object_shape_stamp(obj);
assert_eq!(before, shapes::object_shape_stamp(sibling));
let value = crate::value::js_nanbox_pointer(obj as i64);
operation(value);
let after = shapes::object_shape_stamp(obj);
assert_ne!(before, after, "{name} must retire the extensible shape");
assert_eq!(
before,
shapes::object_shape_stamp(sibling),
"{name} must not change a sibling's shape"
);
operation(value);
assert_eq!(
after,
shapes::object_shape_stamp(obj),
"repeated {name} must not mint another shape for unchanged flags"
);
}
}
}

#[test]
fn rule1_set_property_attrs_transitions() {
assert_shape_moves("set_property_attrs", |addr| {
Expand Down
Loading