diff --git a/changelog.d/8148-typedarray-dynamic-index-header-owner.md b/changelog.d/8148-typedarray-dynamic-index-header-owner.md new file mode 100644 index 0000000000..1b29bd07e7 --- /dev/null +++ b/changelog.d/8148-typedarray-dynamic-index-header-owner.md @@ -0,0 +1,71 @@ +`js_typed_array_index_get_dynamic` answered `undefined` for a receiver its own +classifier had just accepted as a typed array. + +The two receiver gates in the element-READ path are not the same predicate. +`classify_element_read_receiver` (`typedarray/mod.rs`, #8109) answers +`TypedArray` on a registry hit **or** on a `GC_TYPE_TYPED_ARRAY` / +`GC_TYPE_NATIVE_TYPED_VIEW` managed header — deliberately, and its doc comment +states the intent: "a lookup failure can only cost the diversion, never the +element read". `typed_array_addr_from_value` (`typedarray_props.rs`) gates on +`typed_array_owner_kind`, i.e. the thread-local registry alone. Where those +disagree, `typed_array_index_get_dynamic` fell through to `_` and returned +`undefined`. + +The consequence is an inconsistency between two helpers, not just a dead arm: +codegen emits `js_typed_array_get` for a proven integer index and +`js_typed_array_index_get_dynamic` for a runtime key, on the same receiver in +the same function. `js_typed_array_get` reads a header-established typed array +correctly (it is `classify_element_read_receiver`-backed end to end); its +runtime-key twin did not. + +**Reachability, measured.** No TypeScript-level construction of the +disagreeing receiver was found, and the census says there should not be one. +Both allocation sites register before returning (`typedarray::typed_array_alloc`, +`native_arena::js_native_arena_view` via `register_view`); the only +unregistrations outside tests are GC finalizers for a provably dead object +(`finalize_collected_dead_typed_array` runs post-full-trace, and the sweep's +own comment records that interior block space is not re-allocated before +`BlockCleanup`, so there is no address-reuse ABA) and +`native_arena::unregister_view` (`dispose_owner` does *not* unregister, so a +disposed-owner view stays registered and keeps throwing); and a typed array +cannot cross a thread boundary (`thread::unsupported_transfer_type_name`). +Both internal callers of `js_typed_array_index_get_dynamic` +(`value::dyn_index`, `object::polymorphic_index`, `typed_feedback`) pre-gate on +`lookup_typed_array_kind`, so only the codegen call site can deliver an +unregistered receiver. A live disagreement therefore implies a dangling +pointer to a collected typed array — the #7154 class — where `undefined` was +never the interesting part. A 15-receiver-shape TypeScript probe (plain array, +plain object, string, non-pointer, `Int32Array`, `Float64Array`, `Int16Array` +subarray/slice, `ArrayBuffer` view, offset view, Buffer-backed `Uint8Array`, +`Buffer.from`, `DataView`, `Uint8ClampedArray`, `BigInt64Array`, `map()` +result, symbol/expando/canonical-string keys) is byte-identical to Node +26.5.1 both before and after this change, as expected. + +The fix is therefore consistency, not a repair: `typed_array_index_get_dynamic` +now resolves the classifier's `TypedArray(addr)` into an owner and runs the +same key logic (symbol side table / canonical-index string key / numeric +element) instead of falling through. The registry-gated primitives grew +`_for(owner, kind, …)` forms that take the ALREADY-RESOLVED owner kind, so the +key logic no longer re-derives it and re-loses the header-established +receiver; the registry-resolving wrappers are unchanged, so no other caller's +behaviour moves. + +`crates/perry-runtime/src/typedarray/element_read_receiver_tests.rs` gains six +tests. `header_only_typed_array_is_the_disagreement_8116_names` pins the +fixture (registry MISSES, header WINS). `js_typed_array_get_reads_the_sibling_ +of_the_dynamic_hole` is the control that makes the arm worth closing — it +passes with and without the fix, and shows the same receiver already reads +correctly through the constant-index helper. Three tests assert recovered +VALUES through the dynamic helper (`70000 & 0xFFFF == 4464`, so a fallback +that read boxed-f64 slots instead of 16-bit lanes fails them), covering the +numeric key, the canonical numeric-index string key and a Symbol key. Two +controls guard over-reach: `Absent` receivers stay `undefined`, and a +`Uint8ArrayBuffer` owner still reads its bytes through `js_buffer_get` +(different payload offset — serving it through the typed-array lane reader +would not answer 250). + +Not fixed here, found by the same probe and filed as #8149: a `DataView` +and a raw `ArrayBuffer` are byte-indexable in Perry (`(dv as any)[0]` is `0`, +Node's `undefined`), and `Object.keys(buffer)` is `[]` where Node lists the +byte indices. Both reproduce with no typed-array static hint at all, so they +live in `js_dyn_index_get`'s registered-buffer arm, not here. diff --git a/crates/perry-runtime/src/typedarray/element_read_receiver_tests.rs b/crates/perry-runtime/src/typedarray/element_read_receiver_tests.rs index e01ae96141..c895d50d5f 100644 --- a/crates/perry-runtime/src/typedarray/element_read_receiver_tests.rs +++ b/crates/perry-runtime/src/typedarray/element_read_receiver_tests.rs @@ -359,3 +359,153 @@ fn uint8_helpers_still_serve_a_uint8_clamped_array() { js_uint8array_set(recv as *mut TypedArrayHeader, 0, 300); assert_eq!(js_uint8array_index_get_value(recv, 0), 255.0); } + +// -------------------------------------------------------------------------- +// #8116: the two receiver gates the READ helpers use are NOT the same +// predicate. +// +// * `classify_element_read_receiver` answers `TypedArray` on a registry hit +// OR on a `GC_TYPE_TYPED_ARRAY` / `GC_TYPE_NATIVE_TYPED_VIEW` managed +// header — deliberately, so "a lookup failure can only cost the diversion, +// never the element read". +// * `typed_array_addr_from_value` (`typedarray_props.rs`) gates on +// `typed_array_owner_kind`, i.e. the registry alone. +// +// Where they disagree, `typed_array_index_get_dynamic` fell through to `_` +// and answered `undefined` — so the two READ helpers CONTRADICTED each other +// on the same receiver: `js_typed_array_get` (emitted for a proven integer +// index) read the element, `js_typed_array_index_get_dynamic` (emitted for a +// runtime key, same receiver, same function) did not. +// +// REACHABILITY. No TypeScript-level construction of a disagreeing receiver is +// known, and the census says there should not be one: both allocation sites +// register before returning (`typed_array_alloc`, +// `native_arena::js_native_arena_view`), the only non-test unregistrations are +// GC finalizers for a provably dead object, and a typed array cannot cross a +// thread boundary. `js_typed_array_get_reads_the_sibling_of_the_dynamic_hole` +// below is what makes the arm worth closing anyway: it is an inconsistency +// between two helpers that share a classifier and a documented contract, not +// a repair of a live read. +// +// The state is built the only way it can occur — allocation kept, registry +// entry dropped — and every assertion is a VALUE, chosen so the pre-fix body +// cannot pass: `70000 & 0xFFFF == 4464` proves the per-kind 16-bit lane load +// ran rather than a boxed-f64 slot read. +// -------------------------------------------------------------------------- + +/// A receiver whose managed GC header says typed array while the registry +/// does not know it — the exact disagreement #8116 names. Elements are +/// written BEFORE the registry entry is dropped, so the payload is a real +/// per-kind lane store. +fn header_only_typed_array(kind: u8, values: &[f64]) -> *mut TypedArrayHeader { + let ta = typed(kind, values); + unregister_typed_array(ta); + ta +} + +#[test] +fn header_only_typed_array_is_the_disagreement_8116_names() { + let ta = header_only_typed_array(UINT16, &[1.0, 2.0]); + let addr = ta as usize; + + assert!( + lookup_typed_array_kind(addr).is_none(), + "the registry must MISS, or this fixture is not the #8116 receiver" + ); + assert!( + matches!( + classify_element_read_receiver(addr as u64), + ElementReadReceiver::TypedArray(a) if a == addr + ), + "the managed header must still WIN — that is the half of the \ + disagreement `classify_element_read_receiver` documents" + ); +} + +#[test] +fn js_typed_array_get_reads_the_sibling_of_the_dynamic_hole() { + // The control that makes the #8116 arm worth closing: the SAME receiver + // is already read correctly by the constant-index helper. Codegen chooses + // between the two on nothing but whether the key is a proven integer, so + // an `undefined` from the dynamic twin is a contradiction, not a policy. + // This assertion holds with and without the fix. + let ta = header_only_typed_array(UINT16, &[1.0, 70000.0]); + assert_eq!(js_typed_array_get(as_recv(ta), 1), 4464.0); +} + +#[test] +fn js_typed_array_index_get_dynamic_reads_a_header_only_typed_array() { + let ta = header_only_typed_array(UINT16, &[1.0, 70000.0]); + let recv = as_recv(ta); + + // Pre-fix: `undefined` for every one of these. + assert_eq!(js_typed_array_index_get_dynamic(recv, 1.0), 4464.0); + assert_eq!(js_typed_array_index_get_dynamic(recv, 0.0), 1.0); + // A canonical numeric-index STRING key is an element read too. + let key = crate::string::js_string_from_str("1"); + assert_eq!( + js_typed_array_index_get_dynamic(recv, crate::value::js_nanbox_string(key as i64)), + 4464.0 + ); + // Out of bounds stays `undefined` — the IntegerIndexedExotic answer, not + // a read past the end of the payload. + assert!(is_undefined(js_typed_array_index_get_dynamic(recv, 2.0))); + assert!(is_undefined(js_typed_array_index_get_dynamic(recv, -1.0))); +} + +#[test] +fn js_typed_array_index_get_dynamic_reads_a_symbol_key_off_a_header_only_typed_array() { + let ta = header_only_typed_array(UINT16, &[5.0, 6.0]); + let recv = as_recv(ta); + let boxed = crate::value::js_nanbox_pointer(ta as i64); + + let sym = unsafe { crate::symbol::js_symbol_new_empty() }; + unsafe { + crate::symbol::js_object_set_symbol_property(boxed, sym, 1234.0); + } + // Pre-fix the `_` arm answered before the symbol side table was ever + // consulted, so this read was `undefined`. + assert_eq!(js_typed_array_index_get_dynamic(recv, sym), 1234.0); +} + +// -------------------------------------------------------------------------- +// Controls for the #8116 change: it must not widen the receiver funnel, and +// threading the resolved owner KIND through the key logic must not collapse +// the two owner representations into one. +// -------------------------------------------------------------------------- + +#[test] +fn js_typed_array_index_get_dynamic_is_still_undefined_for_a_non_pointer_receiver() { + // `Absent` must stay `undefined`: "make the helper decline everything" + // would pass the tests above, and "make it accept everything" would fail + // this one. + assert!(is_undefined(js_typed_array_index_get_dynamic( + std::ptr::null(), + 0.0 + ))); + assert!(is_undefined(js_typed_array_index_get_dynamic( + 0x3F as *const TypedArrayHeader, + 0.0 + ))); +} + +#[test] +fn js_typed_array_index_get_dynamic_still_reads_a_uint8array_buffer_owner() { + // The OTHER owner representation (`TypedArrayOwnerKind::Uint8ArrayBuffer`, + // a Buffer-backed `Uint8Array` — #5989). Its bytes live at a different + // offset than a `TypedArrayHeader`'s inline storage, so serving it through + // the typed-array lane reader would answer something other than 250. + let buf = crate::buffer::buffer_alloc(3); + unsafe { + (*buf).length = 3; + } + crate::buffer::js_buffer_set(buf, 0, 250); + crate::buffer::js_buffer_set(buf, 2, 7); + crate::buffer::mark_as_uint8array(buf as usize); + assert!(crate::buffer::is_uint8array_buffer(buf as usize)); + + let recv = ((buf as u64) & POINTER_MASK) as *const TypedArrayHeader; + assert_eq!(js_typed_array_index_get_dynamic(recv, 0.0), 250.0); + assert_eq!(js_typed_array_index_get_dynamic(recv, 2.0), 7.0); + assert!(is_undefined(js_typed_array_index_get_dynamic(recv, 3.0))); +} diff --git a/crates/perry-runtime/src/typedarray_props.rs b/crates/perry-runtime/src/typedarray_props.rs index 24c0c0f223..3a77a93617 100644 --- a/crates/perry-runtime/src/typedarray_props.rs +++ b/crates/perry-runtime/src/typedarray_props.rs @@ -42,12 +42,28 @@ fn typed_array_owner_kind(owner: usize) -> Option { } } -unsafe fn typed_array_owner_length(owner: usize) -> u32 { - match typed_array_owner_kind(owner) { - Some(TypedArrayOwnerKind::TypedArray) => (*(owner as *const TypedArrayHeader)).length, - Some(TypedArrayOwnerKind::Uint8ArrayBuffer) => { +/// `[[ArrayLength]]` for an owner whose kind the CALLER already resolved. +/// +/// #8116: the registry is not the only thing that can establish "this is a +/// typed array" — `classify_element_read_receiver` also accepts a +/// `GC_TYPE_TYPED_ARRAY` / `GC_TYPE_NATIVE_TYPED_VIEW` managed header, and +/// promises that a registry miss "can only cost the diversion, never the +/// element read". Taking the kind as a parameter is what lets the element +/// read honour that promise: re-deriving it from `typed_array_owner_kind` +/// here would answer `0` for exactly the receiver the classifier just +/// accepted. +unsafe fn typed_array_owner_length_for(owner: usize, kind: TypedArrayOwnerKind) -> u32 { + match kind { + TypedArrayOwnerKind::TypedArray => (*(owner as *const TypedArrayHeader)).length, + TypedArrayOwnerKind::Uint8ArrayBuffer => { crate::buffer::js_buffer_length(owner as *const crate::buffer::BufferHeader) as u32 } + } +} + +unsafe fn typed_array_owner_length(owner: usize) -> u32 { + match typed_array_owner_kind(owner) { + Some(kind) => typed_array_owner_length_for(owner, kind), None => 0, } } @@ -86,15 +102,26 @@ pub(crate) unsafe fn species_result_store(owner: usize, index: usize, raw: f64) } } -unsafe fn typed_array_owner_get(owner: usize, index: u32) -> f64 { - match typed_array_owner_kind(owner) { - Some(TypedArrayOwnerKind::TypedArray) => { +/// Element read for an owner whose kind the CALLER already resolved. See +/// [`typed_array_owner_length_for`] for why the kind is a parameter. +/// +/// `js_typed_array_get` is itself `classify_element_read_receiver`-backed, so +/// the `TypedArray` arm serves a header-established receiver correctly (#8116). +unsafe fn typed_array_owner_get_for(owner: usize, kind: TypedArrayOwnerKind, index: u32) -> f64 { + match kind { + TypedArrayOwnerKind::TypedArray => { js_typed_array_get(owner as *const TypedArrayHeader, index as i32) } - Some(TypedArrayOwnerKind::Uint8ArrayBuffer) => { + TypedArrayOwnerKind::Uint8ArrayBuffer => { crate::buffer::js_buffer_get(owner as *const crate::buffer::BufferHeader, index as i32) as f64 } + } +} + +unsafe fn typed_array_owner_get(owner: usize, index: u32) -> f64 { + match typed_array_owner_kind(owner) { + Some(kind) => typed_array_owner_get_for(owner, kind, index), None => f64::from_bits(crate::value::TAG_UNDEFINED), } } @@ -598,13 +625,39 @@ pub(crate) unsafe fn typed_array_get_own_property_value( typed_array_get_property_value_by_name(owner, name) } +/// String-key own-property read for an owner whose kind the CALLER already +/// resolved. See [`typed_array_owner_length_for`] (#8116). +unsafe fn typed_array_get_own_property_value_for( + ta: *const TypedArrayHeader, + kind: TypedArrayOwnerKind, + key: *const crate::string::StringHeader, +) -> Option { + if ta.is_null() || key.is_null() { + return None; + } + let name = string_header_str(key)?; + typed_array_get_property_value_by_name_for(ta as usize, kind, name) +} + pub(crate) unsafe fn typed_array_get_property_value_by_name( owner: usize, name: &str, ) -> Option { - typed_array_owner_kind(owner)?; - match typed_array_string_key_kind(name, typed_array_owner_length(owner)) { - TypedArrayStringKeyKind::InBoundsIndex(index) => Some(typed_array_owner_get(owner, index)), + let kind = typed_array_owner_kind(owner)?; + typed_array_get_property_value_by_name_for(owner, kind, name) +} + +/// String-key `[[Get]]` for an owner whose kind the CALLER already resolved. +/// See [`typed_array_owner_length_for`] (#8116). +unsafe fn typed_array_get_property_value_by_name_for( + owner: usize, + kind: TypedArrayOwnerKind, + name: &str, +) -> Option { + match typed_array_string_key_kind(name, typed_array_owner_length_for(owner, kind)) { + TypedArrayStringKeyKind::InBoundsIndex(index) => { + Some(typed_array_owner_get_for(owner, kind, index)) + } TypedArrayStringKeyKind::IntegerIndex => Some(f64::from_bits(crate::value::TAG_UNDEFINED)), TypedArrayStringKeyKind::Ordinary => { let prop = typed_array_own_prop_snapshot(owner, name)?; @@ -627,48 +680,100 @@ pub(crate) unsafe fn typed_array_get_property_value_by_name( } pub(crate) unsafe fn typed_array_get_numeric_index(owner: usize, index: f64) -> Option { - typed_array_owner_kind(owner)?; + let kind = typed_array_owner_kind(owner)?; + Some(typed_array_get_numeric_index_for(owner, kind, index)) +} + +/// IntegerIndexedExotic `[[Get]]` for an owner whose kind the CALLER already +/// resolved. See [`typed_array_owner_length_for`] (#8116). +unsafe fn typed_array_get_numeric_index_for( + owner: usize, + kind: TypedArrayOwnerKind, + index: f64, +) -> f64 { if !index.is_finite() || index.fract() != 0.0 || index < 0.0 || index > u32::MAX as f64 { - return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); + return f64::from_bits(crate::value::TAG_UNDEFINED); } let index = index as u32; - if index < typed_array_owner_length(owner) { - Some(typed_array_owner_get(owner, index)) + if index < typed_array_owner_length_for(owner, kind) { + typed_array_owner_get_for(owner, kind, index) } else { - Some(f64::from_bits(crate::value::TAG_UNDEFINED)) + f64::from_bits(crate::value::TAG_UNDEFINED) } } pub(crate) unsafe fn typed_array_index_get_dynamic(owner_bits: usize, key: f64) -> f64 { - let Some(owner) = typed_array_addr_from_value(f64::from_bits(owner_bits as u64)) else { - // #5989: a `u8.subarray(...)` / `u8.slice(...)` of a BufferHeader-backed - // Uint8Array returns another (uint8array-marked) BUFFER, which the - // typed-array registry gate above doesn't know — a statically-typed - // `r[i]` on such a value silently read `undefined` (react-server-dom's - // flight row parser walks exactly these chunk views). Route buffer - // receivers through the generic dynamic index path, which handles - // BufferHeader indexing (numeric, string, and symbol keys) correctly. - let addr = owner_bits & crate::value::POINTER_MASK as usize; - if addr != 0 && crate::buffer::is_registered_buffer(addr) { - return crate::value::js_dyn_index_get( - crate::value::js_nanbox_pointer(addr as i64), - key, - ); - } - // #8100: the variable-key twin of the `js_typed_array_get` bug. Codegen - // emits this helper for a reassigned local whose DECLARED type is a - // typed array, so the receiver is routinely a plain array/object; the - // pre-#8100 arm answered `undefined` for all of them. Classify the raw - // address and take the ordinary `[[Get]]` when it is not a typed - // array. No recursion: `js_dyn_index_get` re-enters this function only - // when `lookup_typed_array_kind` succeeds, which is precisely the case - // `classify_element_read_receiver` keeps on the typed path. - return match crate::typedarray::classify_element_read_receiver(owner_bits as u64) { - crate::typedarray::ElementReadReceiver::Ordinary(receiver) => { - crate::value::js_dyn_index_get(receiver, key) + let resolved = typed_array_addr_from_value(f64::from_bits(owner_bits as u64)) + .and_then(|owner| typed_array_owner_kind(owner).map(|kind| (owner, kind))); + let (owner, kind) = match resolved { + Some(pair) => pair, + None => { + // #5989: a `u8.subarray(...)` / `u8.slice(...)` of a + // BufferHeader-backed Uint8Array returns another + // (uint8array-marked) BUFFER, which the typed-array registry gate + // above doesn't know — a statically-typed `r[i]` on such a value + // silently read `undefined` (react-server-dom's flight row parser + // walks exactly these chunk views). Route buffer receivers through + // the generic dynamic index path, which handles BufferHeader + // indexing (numeric, string, and symbol keys) correctly. + let addr = owner_bits & crate::value::POINTER_MASK as usize; + if addr != 0 && crate::buffer::is_registered_buffer(addr) { + return crate::value::js_dyn_index_get( + crate::value::js_nanbox_pointer(addr as i64), + key, + ); } - _ => f64::from_bits(crate::value::TAG_UNDEFINED), - }; + // #8100: the variable-key twin of the `js_typed_array_get` bug. + // Codegen emits this helper for a reassigned local whose DECLARED + // type is a typed array, so the receiver is routinely a plain + // array/object; the pre-#8100 arm answered `undefined` for all of + // them. Classify the raw address and take the ordinary `[[Get]]` + // when it is not a typed array. No recursion: `js_dyn_index_get` + // re-enters this function only when `lookup_typed_array_kind` + // succeeds, which is precisely the case + // `classify_element_read_receiver` keeps on the typed path. + match crate::typedarray::classify_element_read_receiver(owner_bits as u64) { + crate::typedarray::ElementReadReceiver::Ordinary(receiver) => { + return crate::value::js_dyn_index_get(receiver, key) + } + // #8116: the two receiver gates are NOT the same predicate. + // `classify_element_read_receiver` also accepts a + // `GC_TYPE_TYPED_ARRAY` / `GC_TYPE_NATIVE_TYPED_VIEW` managed + // header, on purpose, so that "a lookup failure can only cost + // the diversion, never the element read" — while + // `typed_array_addr_from_value` gates on + // `typed_array_owner_kind`, i.e. the registry alone. Where they + // disagree this arm used to answer `undefined`, which made the + // two READ helpers contradict each other on the same receiver: + // `js_typed_array_get` (constant index) reads the element, + // `js_typed_array_index_get_dynamic` (runtime key) did not. + // Codegen picks between them on nothing but whether the key is + // a proven integer. Resolve the classifier's answer into an + // owner and run the same key logic instead. + // + // REACHABILITY (2026-08-15, #8116): no TypeScript-level + // construction of the disagreeing receiver is known. Every + // `GC_TYPE_TYPED_ARRAY` / `GC_TYPE_NATIVE_TYPED_VIEW` + // allocation registers before it is returned + // (`typedarray::typed_array_alloc`, + // `native_arena::js_native_arena_view`), the only + // unregistrations outside tests are GC finalizers for a + // provably dead object, and a typed array cannot cross a + // thread boundary (`thread::unsupported_transfer_type_name`). + // So a live disagreement means a dangling pointer to a + // collected typed array — the #7154 class — where `undefined` + // was never the interesting part. This arm is therefore + // consistency, not a repair: it cannot regress a reachable + // read, and `element_read_receiver_tests.rs` pins it against a + // receiver built in exactly that state. + crate::typedarray::ElementReadReceiver::TypedArray(addr) => { + (addr, TypedArrayOwnerKind::TypedArray) + } + crate::typedarray::ElementReadReceiver::Absent => { + return f64::from_bits(crate::value::TAG_UNDEFINED) + } + } + } }; // A Symbol key is never an integer-indexed element — read it from the symbol // side table, exactly as the ordinary `obj[sym]` path does. Without this a @@ -689,7 +794,7 @@ pub(crate) unsafe fn typed_array_index_get_dynamic(owner_bits: usize, key: f64) return f64::from_bits(crate::value::TAG_UNDEFINED); } if let Some(value) = - typed_array_get_own_property_value(owner as *const TypedArrayHeader, key_ptr) + typed_array_get_own_property_value_for(owner as *const TypedArrayHeader, kind, key_ptr) { return value; } @@ -699,12 +804,10 @@ pub(crate) unsafe fn typed_array_index_get_dynamic(owner_bits: usize, key: f64) ); } if jsval.is_int32() { - return typed_array_get_numeric_index(owner, jsval.as_int32() as f64) - .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); + return typed_array_get_numeric_index_for(owner, kind, jsval.as_int32() as f64); } if key.is_finite() { - return typed_array_get_numeric_index(owner, key) - .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); + return typed_array_get_numeric_index_for(owner, kind, key); } f64::from_bits(crate::value::TAG_UNDEFINED) }