-
-
Notifications
You must be signed in to change notification settings - Fork 159
fix(runtime): own-key order + observable trap order for Object enumeration; array symbol-keyed properties (#5901) #7467
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| fix(runtime): own-property enumeration order + observable trap order for `Object` statics; array symbol-keyed properties (#5901, PR #7467) | ||
|
|
||
| - Symbol keys now keep property-**creation** order across a data→accessor | ||
| `defineProperty` redefine, and for accessors installed between two data | ||
| installs: `set_symbol_accessor_property` leaves an order-preserving | ||
| placeholder in `SYMBOL_PROPERTIES` (value readers all consult the accessor | ||
| table first; `clone_symbol_entries_for_obj_ptr` filters placeholders for | ||
| the raw-entry consumers). test262: | ||
| `getOwnPropertySymbols/order-after-define-property`. | ||
| - `Object.values` / `Object.entries` on a Proxy fire one `ownKeys` trap, then | ||
| interleave `getOwnPropertyDescriptor` + `get` per key per | ||
| EnumerableOwnPropertyNames, instead of batching all descriptor reads first. | ||
| test262: `values/observable-operations`, `entries/observable-operations`. | ||
| - `Object.getOwnPropertyDescriptors` on a Proxy fires `ownKeys` once (the | ||
| generic string/symbol two-helper enumeration fired an observable second | ||
| trap) and reads descriptors in the trap result's verbatim key order. | ||
| test262: `getOwnPropertyDescriptors/observable-operations`. | ||
| - Arrays support symbol-keyed properties: `arr[sym] = v` was silently | ||
| dropped (no symbol arm in `js_array_set_index_or_string`) and `arr[sym]` | ||
| hard-returned `undefined`; both now route through the symbol side table | ||
| like plain-object receivers. | ||
|
|
||
| Validation: new sabotage-verified unit test | ||
| (`symbol_keys_keep_creation_order_across_accessor_redefine`); perry-runtime | ||
| `--lib` 1655/1655; test262 `built-ins/Object` slice 3141→3149 pass with only | ||
| removals in the failure diff; `built-ins/Array` slice swept — remaining | ||
| failures all predate the change (#5898 snapshot cross-check). |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -468,23 +468,72 @@ fn for_each_string_char<F: FnMut(u32, f64)>(value: f64, mut emit: F) -> Option<u | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// handle-band payload, not an address — either got dereferenced (SIGSEGV) or, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// once the handle-band guard rejected it, silently reported no properties. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unsafe fn proxy_values_or_entries(value: f64, want_pairs: bool) -> *mut ArrayHeader { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let keys_boxed = crate::proxy::proxy_enum_own_keys(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // EnumerableOwnPropertyNames(O, value / key+value) on a Proxy: ONE | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // `ownKeys` trap, then — per string key — `getOwnPropertyDescriptor` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // followed immediately by `get` when the descriptor is enumerable. The | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // traps must interleave per key (test262 values/entries | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // observable-operations); routing through `proxy_enum_own_keys` batched | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // every descriptor read before the first `get` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // (|gOPD:a|gOPD:b|gOPD:c|get:a|…). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Both trap calls run user code that can GC, so the receiver, the key | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // list, the result array, and the per-iteration key/value all live in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // handles and are re-read after each call. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let scope = crate::gc::RuntimeHandleScope::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let recv_h = scope.root_nanbox_f64(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let keys_boxed = crate::proxy::js_proxy_own_keys(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let keys_arr = (keys_boxed.to_bits() & crate::value::POINTER_MASK) as *mut ArrayHeader; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let len = crate::array::js_array_length(keys_arr); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut out = crate::array::js_array_alloc(len.max(1) as u32); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let keys_h = scope.root_raw_mut_ptr(keys_arr); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let len = crate::array::js_array_length(keys_h.get_raw_const_ptr::<ArrayHeader>()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let out_h = scope.root_raw_mut_ptr(crate::array::js_array_alloc(len.max(1) as u32)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let key_h = scope.root_nanbox_f64(f64::from_bits(crate::value::TAG_UNDEFINED)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Allocated once and rewritten per iteration so an N-key proxy doesn't | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // push N slots onto the handle stack (same discipline as | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // `js_object_get_own_property_descriptors`). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let val_h = scope.root_nanbox_f64(f64::from_bits(crate::value::TAG_UNDEFINED)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i in 0..len { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let key = crate::array::js_array_get(keys_arr, i); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let val = crate::proxy::js_proxy_get(value, f64::from_bits(key.bits())); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let key = crate::array::js_array_get(keys_h.get_raw_const_ptr::<ArrayHeader>(), i); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !key.is_any_string() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; // symbol keys are excluded from values/entries | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key_h.set_nanbox_u64(key.bits()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let desc = crate::proxy::js_reflect_get_own_property_descriptor( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recv_h.get_nanbox_f64(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key_h.get_nanbox_f64(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if desc.to_bits() == crate::value::TAG_UNDEFINED { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let desc_ptr = (desc.to_bits() & crate::value::POINTER_MASK) as *const ObjectHeader; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if desc_ptr.is_null() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let ek = crate::string::js_string_from_bytes(b"enumerable".as_ptr(), 10); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if crate::value::js_is_truthy(crate::object::js_object_get_field_by_name_f64(desc_ptr, ek)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| == 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let val = crate::proxy::js_proxy_get(recv_h.get_nanbox_f64(), key_h.get_nanbox_f64()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val_h.set_nanbox_f64(val); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+500
to
+518
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Root the descriptor before creating the
Add a descriptor handle immediately after the undefined check. Create the Proposed fix if desc.to_bits() == crate::value::TAG_UNDEFINED {
continue;
}
- let desc_ptr = (desc.to_bits() & crate::value::POINTER_MASK) as *const ObjectHeader;
+ let desc_h = scope.root_nanbox_f64(desc);
+ let ek = crate::string::js_string_from_bytes(b"enumerable".as_ptr(), 10);
+ let desc_ptr =
+ (desc_h.get_nanbox_f64().to_bits() & crate::value::POINTER_MASK) as *const ObjectHeader;
if desc_ptr.is_null() {
continue;
}
- let ek = crate::string::js_string_from_bytes(b"enumerable".as_ptr(), 10);As per coding guidelines, “GC-managed values must remain rooted across every possible collection point.” 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if want_pairs { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let pair = crate::array::js_array_alloc(2); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let pair = crate::array::js_array_push(pair, key); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let pair = crate::array::js_array_push_f64(pair, val); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| out = crate::array::js_array_push(out, JSValue::array_ptr(pair)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let pair = crate::array::js_array_push_f64(pair, key_h.get_nanbox_f64()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let pair = crate::array::js_array_push_f64(pair, val_h.get_nanbox_f64()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let pushed = crate::array::js_array_push( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| out_h.get_raw_mut_ptr::<ArrayHeader>(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JSValue::array_ptr(pair), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| out_h.set_raw_mut_ptr(pushed); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| out = crate::array::js_array_push_f64(out, val); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let pushed = crate::array::js_array_push_f64( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| out_h.get_raw_mut_ptr::<ArrayHeader>(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val_h.get_nanbox_f64(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| out_h.set_raw_mut_ptr(pushed); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| out | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| out_h.get_raw_mut_ptr::<ArrayHeader>() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Tag-dispatching `Object.values(value)` — see [`js_object_keys_value`]. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -539,6 +539,85 @@ fn symbol_define_property_attrs_round_trip_descriptor() { | |
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn symbol_keys_keep_creation_order_across_accessor_redefine() { | ||
| // `[[OwnPropertyKeys]]` reports symbol keys in property-CREATION order. A | ||
| // data→accessor redefine must not move the key to the end (test262 | ||
| // getOwnPropertySymbols/order-after-define-property), and an accessor | ||
| // installed BETWEEN two data installs must enumerate at its install | ||
| // position — both rest on the order-preserving placeholder that | ||
| // `set_symbol_accessor_property` leaves in `SYMBOL_PROPERTIES`. | ||
| let _global = crate::gc::global_side_table_test_lock(); | ||
| crate::symbol::test_clear_symbol_side_table_roots(); | ||
| unsafe { | ||
| let own_symbol_order = |obj_value: f64| -> Vec<usize> { | ||
| let arr = crate::symbol::js_object_get_own_property_symbols(obj_value) | ||
| as *const crate::array::ArrayHeader; | ||
| assert!(!arr.is_null()); | ||
| let n = crate::array::js_array_length(arr); | ||
| (0..n) | ||
| .map(|i| { | ||
| (crate::array::js_array_get(arr, i).bits() & crate::value::POINTER_MASK) | ||
| as usize | ||
| }) | ||
| .collect() | ||
| }; | ||
| let getter_descriptor = || -> f64 { | ||
| let getter = crate::closure::js_closure_alloc(closure_accessor_getter as *const u8, 0); | ||
| assert!(!getter.is_null()); | ||
| let get_key = crate::string::js_string_from_bytes(b"get".as_ptr(), 3); | ||
| let descriptor = js_object_alloc(0, 0); | ||
| assert!(!descriptor.is_null()); | ||
| js_object_set_field_by_name( | ||
| descriptor, | ||
| get_key, | ||
| crate::value::js_nanbox_pointer(getter as i64), | ||
| ); | ||
| crate::value::js_nanbox_pointer(descriptor as i64) | ||
| }; | ||
|
|
||
| // Data → accessor redefine keeps the key's position. | ||
| let obj = js_object_alloc(0, 0); | ||
| assert!(!obj.is_null()); | ||
| let obj_value = crate::value::js_nanbox_pointer(obj as i64); | ||
| let sym_a = crate::symbol::js_symbol_new_empty(); | ||
| let sym_b = crate::symbol::js_symbol_new_empty(); | ||
| let a_ptr = crate::symbol::sym_key_from_f64(sym_a); | ||
| let b_ptr = crate::symbol::sym_key_from_f64(sym_b); | ||
| crate::symbol::js_object_set_symbol_property(obj_value, sym_a, 1.0); | ||
| crate::symbol::js_object_set_symbol_property(obj_value, sym_b, 2.0); | ||
| js_object_define_property(obj_value, sym_a, getter_descriptor()); | ||
| assert_eq!( | ||
| own_symbol_order(obj_value), | ||
| vec![a_ptr, b_ptr], | ||
| "data→accessor redefine moved the key out of creation order" | ||
| ); | ||
| // The placeholder must never serve as the value — the read goes | ||
| // through the accessor table and runs the getter. | ||
| let read = crate::symbol::js_object_get_symbol_property(obj_value, sym_a); | ||
| assert_eq!(read.to_bits(), 4.0f64.to_bits()); | ||
|
|
||
| // Accessor installed between two data installs enumerates in place. | ||
| let obj2 = js_object_alloc(0, 0); | ||
| assert!(!obj2.is_null()); | ||
| let obj2_value = crate::value::js_nanbox_pointer(obj2 as i64); | ||
| let sym_c = crate::symbol::js_symbol_new_empty(); | ||
| let sym_d = crate::symbol::js_symbol_new_empty(); | ||
| let sym_e = crate::symbol::js_symbol_new_empty(); | ||
| let c_ptr = crate::symbol::sym_key_from_f64(sym_c); | ||
| let d_ptr = crate::symbol::sym_key_from_f64(sym_d); | ||
| let e_ptr = crate::symbol::sym_key_from_f64(sym_e); | ||
| crate::symbol::js_object_set_symbol_property(obj2_value, sym_c, 1.0); | ||
| js_object_define_property(obj2_value, sym_d, getter_descriptor()); | ||
| crate::symbol::js_object_set_symbol_property(obj2_value, sym_e, 3.0); | ||
| assert_eq!( | ||
| own_symbol_order(obj2_value), | ||
| vec![c_ptr, d_ptr, e_ptr], | ||
| "interleaved accessor install enumerated out of creation order" | ||
| ); | ||
|
Comment on lines
+553
to
+617
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift Root the test values across allocation-capable calls. This test keeps raw object, symbol, closure, and string pointers across calls that can collect. A moving collection can invalidate Create As per coding guidelines, “GC-managed values must remain rooted across every possible collection point.” 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_object_alloc_and_fields() { | ||
| let obj = js_object_alloc(1, 3); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 10869
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 50370
Reload the symbol setter receiver before the accessor call.
set_symbol_propertycan call a symbol setter closure viajs_closure_call1. That call can GC and evacuate the stored object, butsetstill stores the originalobj_key; the lookup on next iteration uses that stale key. Derive the stored symbol-entry key from the receiver after each rootable collection/accesor-invocation path, or store the receiver handle’s derived pointer instead of reusing the original argument.🤖 Prompt for AI Agents
Source: Learnings