-
-
Notifications
You must be signed in to change notification settings - Fork 159
fix(runtime): computed object keys on class proto and ClassRef (#6945) #7134
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,30 @@ | ||
| **runtime:** computed / dynamic object-key property access on class prototypes | ||
| and class constructors now matches Node (#6945). | ||
|
|
||
| Three cooperating gaps: | ||
|
|
||
| 1. `js_dyn_index_get` treated non-string, non-numeric keys as floats | ||
| (`format!("{}", f64)`), so `obj[{toString(){return "k"}}]` never ran user | ||
| ToPropertyKey. Object / boolean / null / undefined / bigint keys now go | ||
| through `js_to_property_key` (with receiver rooting) before the by-name | ||
| get — matching the set-side path in `js_dyn_index_set`. | ||
|
|
||
| 2. Class-instance field get walked the reflective decl-proto object only for | ||
| *accessors*, deliberately skipping data reads to avoid class-id re-entry. | ||
| Runtime `C.prototype[k] = v` stores an own data field there, so | ||
| `(new C()).name` missed it while `C.prototype.name` saw it. Own data is | ||
| now read via `own_data_field_by_name` (no re-walk). | ||
|
|
||
| 3. Codegen's IndexGet last-resort path routes non-string keys on a known | ||
| ClassRef through `js_object_get_index_polymorphic`, which rejected every | ||
| INT32-tagged receiver as a primitive. Registered class-ids now forward to | ||
| `js_dyn_index_get`'s class-ref arm so `C[k]` / `C[objectKey]` resolve | ||
| statics and `CLASS_DYNAMIC_PROPS`. | ||
|
|
||
| Regression: `test-files/test_gap_computed_key_class_proto_6945.ts` | ||
| (byte-for-byte vs Node 26.5). | ||
|
|
||
| Follow-up (CodeRabbit): set-side dynamic-index fallback and polymorphic | ||
| `rooted_property_key_{get,set}` now use `js_to_property_key` and route a | ||
| Symbol-yielding `@@toPrimitive` through the symbol store (was silently | ||
| undefined / stringified). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -269,6 +269,45 @@ pub extern "C" fn js_dyn_index_get(value: f64, index: f64) -> f64 { | |
| } | ||
| return f64::from_bits(TAG_UNDEFINED); | ||
| } | ||
| // #6945: a non-string, non-numeric index must run ToPropertyKey (object | ||
| // keys invoke `toString`/`valueOf`/`@@toPrimitive`; booleans/null/ | ||
| // undefined/bigint stringify) before the by-name get. The arms below | ||
| // cast `index as i32` / `format!("{}", index)`, which treat an object | ||
| // NaN-box as a float and never call user coercion — so | ||
| // `proto[{toString(){return "k"}}]` missed a write that | ||
| // `proto.k` / `proto["k"]` could see. Mirrors the set-side | ||
| // `js_jsvalue_to_string` path in `js_dyn_index_set`. | ||
| { | ||
| let idx_js = JSValue::from_bits(idx_bits); | ||
| // INT32-tagged keys are integer property names (and class-ref values | ||
| // used as keys, rare); pure f64 numbers keep the element path. Every | ||
| // other tag is a ToPropertyKey case. | ||
| if !idx_js.is_number() && !idx_js.is_int32() { | ||
| let scope = crate::gc::RuntimeHandleScope::new(); | ||
| let recv = scope.root_raw_mut_ptr(raw_ptr as *mut crate::object::ObjectHeader); | ||
| // Prefer `js_to_property_key` so a Symbol-returning toString is | ||
| // preserved (and then routed via the symbol arm). Root the | ||
| // coerced key: ToPropertyKey can allocate / run user JS. | ||
| let key = unsafe { crate::object::js_to_property_key(index) }; | ||
| let key_h = scope.root_nanbox_f64(key); | ||
| let key = key_h.get_nanbox_f64(); | ||
| if unsafe { crate::symbol::js_is_symbol(key) } != 0 { | ||
| let recv_bits = crate::value::js_nanbox_pointer( | ||
| recv.get_raw_const_ptr::<crate::object::ObjectHeader>() as i64, | ||
| ); | ||
| return unsafe { crate::symbol::js_object_get_symbol_property(recv_bits, key) }; | ||
| } | ||
| let key_ptr = | ||
| crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; | ||
| if key_ptr.is_null() { | ||
| return f64::from_bits(TAG_UNDEFINED); | ||
| } | ||
| return crate::object::js_object_get_field_by_name_f64( | ||
| recv.get_raw_const_ptr::<crate::object::ObjectHeader>(), | ||
| key_ptr, | ||
| ); | ||
| } | ||
| } | ||
| let idx_i32 = if index.is_nan() || index.is_infinite() { | ||
| return f64::from_bits(TAG_UNDEFINED); | ||
| } else { | ||
|
|
@@ -582,18 +621,36 @@ pub extern "C" fn js_dyn_index_set(obj: f64, index: f64, value: f64) -> f64 { | |
| ); | ||
| return value; | ||
| } | ||
| // #6935: this is the corruption case. `js_jsvalue_to_string(index)` runs a | ||
| // user `toString` / `valueOf` for an object index (`obj[{toString(){...}}] = v`) | ||
| // and allocates for every other shape, so it can GC and EVACUATE. Both the | ||
| // #6935: ToPropertyKey (below) runs a user `toString` / `valueOf` / | ||
| // `@@toPrimitive` for an object index (`obj[{toString(){...}}] = v`) and | ||
| // allocates for every other shape, so it can GC and EVACUATE. Both the | ||
| // receiver `raw_ptr` and the `value` being stored were raw Rust locals | ||
| // across it: a stale receiver dropped the write onto a forwarding stub, and | ||
| // a stale `value` wrote a dangling pointer INTO a live object, where it | ||
| // outlives the call. | ||
| // | ||
| // #6945 / CodeRabbit: use `js_to_property_key` (not `js_jsvalue_to_string`) | ||
| // so an `@@toPrimitive` that returns a Symbol is preserved and routed to | ||
| // the symbol store — matching the get-side fallback. Stringifying that | ||
| // Symbol would miss the target property (and Spec ToPropertyKey must not | ||
| // turn a Symbol result into a string). | ||
| let scope = crate::gc::RuntimeHandleScope::new(); | ||
| let recv = scope.root_raw_mut_ptr(raw_ptr as *mut crate::object::ObjectHeader); | ||
| let value_handle = scope.root_nanbox_f64(value); | ||
| let key_ptr = crate::value::js_jsvalue_to_string(index); | ||
| let key = unsafe { crate::object::js_to_property_key(index) }; | ||
| let key_h = scope.root_nanbox_f64(key); | ||
| let key = key_h.get_nanbox_f64(); | ||
| let value = value_handle.get_nanbox_f64(); | ||
| if unsafe { crate::symbol::js_is_symbol(key) } != 0 { | ||
| let recv_bits = crate::value::js_nanbox_pointer( | ||
| recv.get_raw_const_ptr::<crate::object::ObjectHeader>() as i64, | ||
| ); | ||
| unsafe { | ||
| crate::symbol::js_object_set_symbol_property(recv_bits, key, value); | ||
| } | ||
| return value; | ||
| } | ||
| let key_ptr = crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; | ||
|
Comment on lines
+632
to
+653
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. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Preserve symbol keys in the Line 640 fixes only the non-array object fallback. If Add a regression for 🤖 Prompt for AI Agents |
||
| if key_ptr.is_null() { | ||
| return value; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| // #6945: a computed object-key write onto a class prototype (or class | ||
| // constructor) must be visible to: | ||
| // - the computed read with the same key object (ToPropertyKey), | ||
| // - the plain string-key read, | ||
| // - instance inheritance through the prototype chain. | ||
| // Pre-fix, the write landed somewhere the dotted `C.prototype.name` read | ||
| // found, but `C.prototype[k]` and `(new C()).name` returned undefined because | ||
| // `js_dyn_index_get` treated object indices as floats (`format!("{}", f64)`) | ||
| // instead of running ToPropertyKey / user `toString`. | ||
|
|
||
| class C { | ||
| m(): number { | ||
| return 1; | ||
| } | ||
| } | ||
| const k: any = { | ||
| toString(): string { | ||
| return "protoKey"; | ||
| }, | ||
| }; | ||
| (C.prototype as any)[k] = { tag: 6 }; | ||
| console.log("computed via instance:", JSON.stringify((new C() as any).protoKey)); | ||
| console.log("computed direct:", JSON.stringify((C.prototype as any).protoKey)); | ||
| console.log("computed via key obj:", JSON.stringify((C.prototype as any)[k])); | ||
|
|
||
| const plain: any = {}; | ||
| plain[k] = { tag: 7 }; | ||
| console.log("plain:", JSON.stringify(plain.protoKey)); | ||
| console.log("plain via key obj:", JSON.stringify(plain[k])); | ||
|
|
||
| // class-constructor (static) side: same ToPropertyKey obligation | ||
| class D { | ||
| static s(): number { | ||
| return 1; | ||
| } | ||
| } | ||
| const ks: any = { | ||
| toString(): string { | ||
| return "statKey"; | ||
| }, | ||
| }; | ||
| (D as any)[ks] = { tag: 6 }; | ||
| console.log("static via name:", JSON.stringify((D as any).statKey)); | ||
| console.log("static via key obj:", JSON.stringify((D as any)[ks])); | ||
|
|
||
| // coercion is observable even when the property is absent | ||
| let calls = 0; | ||
| const absent: any = { | ||
| toString(): string { | ||
| calls++; | ||
| return "nope"; | ||
| }, | ||
| }; | ||
| console.log("absent via key obj:", JSON.stringify((C.prototype as any)[absent])); | ||
| console.log("absent coercion count:", calls); | ||
|
|
||
| // boolean / null keys also go through ToPropertyKey | ||
| const mixed: any = {}; | ||
| mixed[true as any] = "t"; | ||
| mixed[null as any] = "n"; | ||
| console.log("bool key:", mixed["true"]); | ||
| console.log("null key:", mixed["null"]); | ||
| console.log("bool via true:", mixed[true as any]); | ||
|
|
||
| // @@toPrimitive returning a Symbol must use the symbol store (get + set), | ||
| // not stringify the Symbol (get-side ToPropertyKey parity on set — #7134 CR). | ||
| const sym = Symbol("viaPrim"); | ||
| const viaSym: any = { | ||
| [Symbol.toPrimitive](_hint: string): symbol { | ||
| return sym; | ||
| }, | ||
| }; | ||
| const holder: any = {}; | ||
| holder[viaSym] = { tag: 9 }; | ||
| console.log("viaPrim set+get:", JSON.stringify(holder[viaSym])); | ||
| console.log("viaPrim symbol key:", JSON.stringify(holder[sym])); |
Uh oh!
There was an error while loading. Please reload this page.