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
5 changes: 5 additions & 0 deletions changelog.d/10229-fractional-array-index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix fractional numeric reads on arrays with erased receiver types, including
lazy JSON arrays: `rows[0.5]` now looks up the property `"0.5"` instead of
truncating to element zero. The runtime fallback validates the integer range
before narrowing, preserving negative, non-finite, and large property keys
and reads of named properties without changing the compiler's inline tiers.
120 changes: 120 additions & 0 deletions crates/perry-runtime/src/array/dynamic_numeric_key_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//! #10190: the erased-receiver fallback must preserve the original numeric key.

use super::subclass::js_packed_arraylike_index_get;
use crate::value::js_dyn_index_get;
use crate::value::{JSValue, TAG_UNDEFINED};

fn receiver(state: u8) -> f64 {
if state == 0 {
let mut array = crate::array::js_array_alloc(3);
for value in [11.0, 22.0, 33.0] {
array = crate::array::js_array_push_f64(array, value);
}
return crate::value::js_nanbox_pointer(array as i64);
}
let bytes = b"[11,22,33]";
let tape = crate::json_tape::build_tape(bytes).unwrap();
let text = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32);
let scope = crate::gc::RuntimeHandleScope::new();
let array = scope
.root_raw_mut_ptr(unsafe { crate::json_tape::alloc_lazy_array(&tape.entries, 0, 3, text) });
if state == 2 {
unsafe { crate::json_tape::force_materialize_lazy(array.get_raw_mut_ptr()) };
}
let raw = array.get_raw_const_ptr::<crate::json_tape::LazyArrayHeader>();
let header = unsafe { crate::value::addr_class::try_read_gc_header(raw as usize) }.unwrap();
assert_eq!(header.obj_type, crate::gc::GC_TYPE_LAZY_ARRAY);
assert_eq!(unsafe { !(*raw).materialized.is_null() }, state == 2);
crate::value::js_nanbox_pointer(raw as i64)
}

#[test]
fn non_element_numeric_keys_do_not_truncate_on_regular_lazy_or_materialized_arrays() {
for state in 0..3 {
let scope = crate::gc::RuntimeHandleScope::new();
let array = scope.root_nanbox_f64(receiver(state));
for key in [
0.5,
-0.5,
1.5,
-1.0,
2_147_483_648.0,
4_294_967_295.0,
f64::NAN,
f64::INFINITY,
f64::NEG_INFINITY,
] {
assert_eq!(
js_dyn_index_get(array.get_nanbox_f64(), key).to_bits(),
TAG_UNDEFINED,
"state={state}, key={key}"
);
assert_eq!(
js_packed_arraylike_index_get(array.get_nanbox_f64(), key, std::ptr::null_mut())
.to_bits(),
TAG_UNDEFINED,
"packed state={state}, key={key}"
);
}
for (key, expected) in [
(0.0, 11.0),
(-0.0, 11.0),
(1.0, 22.0),
(f64::from_bits(JSValue::int32(2).bits()), 33.0),
] {
assert_eq!(js_dyn_index_get(array.get_nanbox_f64(), key), expected);
assert_eq!(
js_packed_arraylike_index_get(array.get_nanbox_f64(), key, std::ptr::null_mut()),
expected
);
}
}
}

#[test]
fn numeric_named_properties_remain_readable_through_the_dynamic_fallback() {
let scope = crate::gc::RuntimeHandleScope::new();
let array = scope.root_nanbox_f64(receiver(0));
for (key, name) in [
(0.5, "0.5"),
(-0.5, "-0.5"),
(-1.0, "-1"),
(4_294_967_295.0, "4294967295"),
(f64::NAN, "NaN"),
(f64::INFINITY, "Infinity"),
(1e21, "1e+21"),
] {
let name = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32);
let raw = JSValue::from_bits(array.get_nanbox_f64().to_bits())
.as_pointer::<crate::array::ArrayHeader>()
as *mut crate::array::ArrayHeader;
crate::array::js_array_set_string_key(raw, name, 91.0);
assert_eq!(
js_dyn_index_get(array.get_nanbox_f64(), key),
91.0,
"key={key}"
);
assert_eq!(
js_packed_arraylike_index_get(array.get_nanbox_f64(), key, std::ptr::null_mut()),
91.0,
"packed key={key}"
);
}
assert_eq!(js_dyn_index_get(array.get_nanbox_f64(), 0.0), 11.0);
}

#[test]
fn boxed_integer_keys_keep_their_object_property_semantics() {
let scope = crate::gc::RuntimeHandleScope::new();
let object = scope.root_raw_mut_ptr(crate::object::js_object_alloc(0, 0));
let key = crate::string::js_string_from_bytes(b"2".as_ptr(), 1);
crate::object::js_object_set_field_by_name(object.get_raw_mut_ptr(), key, 42.0);
let read = |key| {
let receiver = crate::value::js_nanbox_pointer(
object.get_raw_const_ptr::<crate::object::ObjectHeader>() as i64,
);
js_dyn_index_get(receiver, key)
};
assert_eq!(read(2.0), 42.0);
assert_eq!(read(f64::from_bits(JSValue::int32(2).bits())), 42.0);
}
2 changes: 2 additions & 0 deletions crates/perry-runtime/src/array/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ mod collection_tag_tests;
#[cfg(test)]
mod dense_move_tests;
#[cfg(test)]
mod dynamic_numeric_key_tests;
#[cfg(test)]
mod forwarding_tests;
#[cfg(test)]
mod index_get_exit_tests;
Expand Down
15 changes: 8 additions & 7 deletions crates/perry-runtime/src/value/dyn_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,13 +408,14 @@ pub extern "C" fn js_dyn_index_get(value: f64, index: f64) -> f64 {
);
}
}
// NaN and +/-Infinity are not array indices, but they are still ordinary
// property keys (`"NaN"`, `"Infinity"`, `"-Infinity"`) on Objects and
// Arrays. Delegate this cold case to the polymorphic key path, which runs
// ToPropertyKey and already distinguishes ordinary from integer-indexed
// exotic receivers. The old early return made a computed definition such
// as `{ [Infinity]: value }` unreadable through `obj[Infinity]`.
if index.is_nan() || index.is_infinite() {
// The element path below takes an i32, so prove an exact non-negative
// index before narrowing. Fractional and negative keys are ordinary
// properties: truncating 0.5 (or -0.5) would read element 0 (#10190).
// Preserve the original key for ToPropertyKey, including non-finite and
// large values; the polymorphic path also handles valid u32 indices
// beyond i32::MAX. NaN-boxed INT32 keys retain their existing decoding
// in that dispatcher, rather than entering the raw-f64 path below.
if !index.is_finite() || index < 0.0 || index > i32::MAX as f64 || index.fract() != 0.0 {
return crate::object::js_object_get_index_polymorphic(raw_ptr as i64, index);
}
let idx_i32 = index as i32;
Expand Down
32 changes: 32 additions & 0 deletions test-files/test_gap_10190_fractional_array_index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// #10190: numeric keys that are not array indices are named properties.
function read(receiver: any, key: any): any { return receiver[key]; }
function numeric(receiver: any, key: number): any { return receiver[key]; }
function typed(receiver: number[], key: number): any { return receiver[key]; }

const rows: any = JSON.parse('[{"id":1},{"id":2},{"id":3}]');
console.log("repro", rows[0.5], rows[-0.5]);
try {
console.log(rows[0.5].id);
} catch (error) {
console.log("repro throws", error instanceof TypeError);
}

const keys = [0.5, -0.5, 1.5, -1, NaN, Infinity, -Infinity,
2147483648, 4294967295, 0, -0, 1, 2, 3];
for (const array of [[11, 22, 33], JSON.parse('[11,22,33]')]) {
for (const key of keys) {
console.log("absent", key, read(array, key), numeric(array, key), typed(array, key));
}
for (let i = 0; i < 9; i++) {
const key = keys[i];
Object.defineProperty(array, String(key), { value: 100 + i, configurable: true });
console.log("own", key, read(array, key), numeric(array, key), typed(array, key));
}
console.log("elements", array.length, array[0], array[1], array[2]);
}

const inherited: any = [11, 22];
const prototype = Object.create(Array.prototype);
Object.defineProperty(prototype, "0.5", { get() { return this[1] + 7; } });
Object.setPrototypeOf(inherited, prototype);
console.log("inherited", read(inherited, 0.5), numeric(inherited, 0.5));
Loading