From be14dd37df60ab3654f34ceaffa6c9f3569dff5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 11 Jun 2026 14:24:05 +0200 Subject: [PATCH] fix(runtime,hir,codegen): TypedArray + TypedArrayConstructors test262 parity (species/detached/ctor/internals) built-ins/TypedArray 778->834 and built-ins/TypedArrayConstructors 453->518 (1231->1350 of 1385 judged, 88.9%->97.5%), zero regressions vs the 48d6f8d3c baseline (set-diff per sweep). Root causes: - CanonicalNumericIndexString classified '1e21'-style keys with Rust format! (full-decimal rendering) instead of JS ToString -> ordinary keys like "1000000000000000000000" / "0.0000001" threw 'Invalid typed array index'. - new TA(buffer, byteOffset, length): ToIndex never ran valueOf and never threw on Symbol/BigInt offsets/lengths; string/bool single-arg lengths built empty arrays. - new TA(plainObject/function): spec iterator-or-array-like resolution (non-callable @@iterator TypeError, ToLength on length incl. abrupt completions, RangeError past 2^32) - function sources were reinterpreted as ArrayHeaders (crash). - %TypedArray%.prototype.toLocaleString: spec per-element Invoke honoring a patched Number.prototype/BigInt.prototype toLocaleString (abrupt completions propagate). - indexOf/lastIndexOf compared boxed BigInt pointers bitwise (never matched); len==0 must precede fromIndex coercion; includes() no-arg passed the NaN sentinel as searchElement. - Default sort scrambled BigInt64/BigUint64 (sorted boxed-pointer bits) and mis-ordered NaN/-0; sort/toSorted/toReversed/copyWithin/with returned raw pointer bits as f64 (String(ta.sort()) -> '2.2e-311'). - SpeciesConstructor: Get(O,'constructor') never consulted the per-kind prototype object, so defineProperty(TA.prototype,'constructor',{get}) getters never fired (also wired into the property-get path). - Integer-Indexed exotic internals: 'in' now walks the prototype chain for ordinary keys; symbol-keyed defineProperty stores value/accessor + attrs in the symbol side tables (string-coercion filed them under 'Symbol(x)'); Reflect.defineProperty TA own-key/extensibility predicates fixed. - Object.preventExtensions(ta) wrote the GC-header flag at addr-8, but small typed arrays are raw-alloc'd with NO GcHeader -> allocator-metadata corruption + random non-extensible reads. Replaced with a side table (typedarray_props), consulted by defineProperty/Reflect/isExtensible. - Reflect.set gained the receiver parameter end-to-end (HIR Expr::ReflectSet, codegen, runtime) and ordinary_set_with_receiver now implements the Integer-Indexed exotic [[Set]]: canonical numeric keys never consult the prototype chain; a receiver distinct from the TA redirects the write (including TA receivers rejecting invalid indices). - %TypedArray%.from: map and per-kind coercion now interleave per element (an abrupt coercion stops later map calls); array-like sources run real ToLength (throwing length getters propagate). - ta.set('567') treats a primitive string as the spec ToObject array-like; TA sources with a patched own valueOf/toString run it for ToNumber/ToBigInt. - join(separator): Symbol separator throws TypeError (both the typed-array and Array.prototype.join_value paths). - arrayBuffer.constructor now answers ArrayBuffer/SharedArrayBuffer instead of Uint8Array. Deferred (diagnosed, out of scope): a codegen-at-scale heisenbug cluster (every/some(NaN) + reduce/forEach crashes that reproduce only under the full 10-ctor x 5-factory harness matrix; deterministic, not GC), invoked-as-func IMPLICIT_THIS staleness, %TypedArray% function-identity asserts, Float16 bit-precision. --- .../perry-codegen-js/src/emit/exprs_more.rs | 9 +- .../perry-codegen/src/expr/proxy_reflect.rs | 14 +- .../src/runtime_decls/objects.rs | 2 +- crates/perry-hir/src/ir/expr.rs | 5 + .../src/lower/expr_call/native_module.rs | 4 + crates/perry-hir/src/stable_hash/expr.rs | 2 +- crates/perry-hir/src/walker/expr_mut.rs | 8 +- crates/perry-hir/src/walker/expr_ref.rs | 8 +- .../perry-runtime/src/array/iter_methods.rs | 5 + crates/perry-runtime/src/array/search.rs | 28 +- crates/perry-runtime/src/builtins/numbers.rs | 13 + .../perry-runtime/src/object/field_get_set.rs | 49 +++- .../perry-runtime/src/object/global_this.rs | 63 +++- .../src/object/native_call_method.rs | 132 ++++++++- crates/perry-runtime/src/object/object_ops.rs | 58 ++++ .../src/object/object_ops_frozen.rs | 19 +- .../src/object/reflect_support.rs | 19 ++ crates/perry-runtime/src/proxy.rs | 68 +++++ crates/perry-runtime/src/proxy/reflect.rs | 24 +- crates/perry-runtime/src/typedarray/bigint.rs | 11 + crates/perry-runtime/src/typedarray/mod.rs | 273 ++++++++++++++++-- .../perry-runtime/src/typedarray/species.rs | 57 +++- crates/perry-runtime/src/typedarray_props.rs | 198 ++++++++++++- crates/perry-runtime/src/typedarray_view.rs | 13 +- 24 files changed, 1000 insertions(+), 82 deletions(-) diff --git a/crates/perry-codegen-js/src/emit/exprs_more.rs b/crates/perry-codegen-js/src/emit/exprs_more.rs index bfe6e5660c..d264d3cbb3 100644 --- a/crates/perry-codegen-js/src/emit/exprs_more.rs +++ b/crates/perry-codegen-js/src/emit/exprs_more.rs @@ -1606,13 +1606,20 @@ impl JsEmitter { self.emit_expr(receiver); self.output.push(')'); } - Expr::ReflectSet { target, key, value } => { + Expr::ReflectSet { + target, + key, + value, + receiver, + } => { self.output.push_str("Reflect.set("); self.emit_expr(target); self.output.push_str(", "); self.emit_expr(key); self.output.push_str(", "); self.emit_expr(value); + self.output.push_str(", "); + self.emit_expr(receiver); self.output.push(')'); } Expr::ReflectHas { target, key } => { diff --git a/crates/perry-codegen/src/expr/proxy_reflect.rs b/crates/perry-codegen/src/expr/proxy_reflect.rs index f6e439035f..35ec0fe83c 100644 --- a/crates/perry-codegen/src/expr/proxy_reflect.rs +++ b/crates/perry-codegen/src/expr/proxy_reflect.rs @@ -416,14 +416,24 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { &[(DOUBLE, &t), (DOUBLE, &k), (DOUBLE, &r)], )) } - Expr::ReflectSet { target, key, value } => { + Expr::ReflectSet { + target, + key, + value, + receiver, + } => { + // Pass the optional receiver through; the runtime defaults an + // `undefined` receiver to the target. A receiver distinct from an + // Integer-Indexed target redirects the write to the receiver per + // OrdinarySet (test262 internals/Set/key-is-valid-index-reflect-set). let t = lower_expr(ctx, target)?; let k = lower_expr(ctx, key)?; let v = lower_expr(ctx, value)?; + let r = lower_expr(ctx, receiver)?; Ok(ctx.block().call( DOUBLE, "js_reflect_set", - &[(DOUBLE, &t), (DOUBLE, &k), (DOUBLE, &v)], + &[(DOUBLE, &t), (DOUBLE, &k), (DOUBLE, &v), (DOUBLE, &r)], )) } Expr::PutValueSet { diff --git a/crates/perry-codegen/src/runtime_decls/objects.rs b/crates/perry-codegen/src/runtime_decls/objects.rs index 744cbbb4f5..b27a0af8ab 100644 --- a/crates/perry-codegen/src/runtime_decls/objects.rs +++ b/crates/perry-codegen/src/runtime_decls/objects.rs @@ -260,7 +260,7 @@ pub fn declare_phase_b_objects(module: &mut LlModule) { module.declare_function("js_proxy_construct", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); module.declare_function("js_reflect_construct", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); module.declare_function("js_reflect_get", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_reflect_set", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); + module.declare_function("js_reflect_set", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE, DOUBLE]); module.declare_function( "js_put_value_set", DOUBLE, diff --git a/crates/perry-hir/src/ir/expr.rs b/crates/perry-hir/src/ir/expr.rs index d0f2d1b8d3..95c68248c4 100644 --- a/crates/perry-hir/src/ir/expr.rs +++ b/crates/perry-hir/src/ir/expr.rs @@ -2371,6 +2371,11 @@ pub enum Expr { target: Box, key: Box, value: Box, + /// Optional `receiver` argument (4th): the object actually written + /// when the target's own/inherited descriptor allows it (observable + /// for Integer-Indexed exotic targets). Lowering supplies `target` + /// when the call omits it. + receiver: Box, }, /// Assignment PutValue for property references. Evaluates target/key/value /// in source order, performs ordinary [[Set]] with an explicit receiver, diff --git a/crates/perry-hir/src/lower/expr_call/native_module.rs b/crates/perry-hir/src/lower/expr_call/native_module.rs index 5c6c1b40f4..1484de61e3 100644 --- a/crates/perry-hir/src/lower/expr_call/native_module.rs +++ b/crates/perry-hir/src/lower/expr_call/native_module.rs @@ -1087,10 +1087,14 @@ pub(super) fn try_native_module_methods( let target = it.next().unwrap_or(Expr::Undefined); let key = it.next().unwrap_or(Expr::Undefined); let value = it.next().unwrap_or(Expr::Undefined); + // Optional `receiver` (4th arg): default `undefined` + // and the runtime substitutes `target`. + let receiver = it.next().unwrap_or(Expr::Undefined); return Ok(Ok(Expr::ReflectSet { target: Box::new(target), key: Box::new(key), value: Box::new(value), + receiver: Box::new(receiver), })); } "has" => { diff --git a/crates/perry-hir/src/stable_hash/expr.rs b/crates/perry-hir/src/stable_hash/expr.rs index b1fd6e8998..167cea3749 100644 --- a/crates/perry-hir/src/stable_hash/expr.rs +++ b/crates/perry-hir/src/stable_hash/expr.rs @@ -598,7 +598,7 @@ impl SH for Expr { Expr::ProxyRevocable { target, handler } => { tag(h, 431); target.as_ref().hash(h); handler.as_ref().hash(h); } Expr::ProxyRevoke(e) => { tag(h, 432); e.as_ref().hash(h); } Expr::ReflectGet { target, key, receiver } => { tag(h, 433); target.as_ref().hash(h); key.as_ref().hash(h); receiver.as_ref().hash(h); } - Expr::ReflectSet { target, key, value } => { tag(h, 434); target.as_ref().hash(h); key.as_ref().hash(h); value.as_ref().hash(h); } + Expr::ReflectSet { target, key, value, receiver } => { tag(h, 434); target.as_ref().hash(h); key.as_ref().hash(h); value.as_ref().hash(h); receiver.as_ref().hash(h); } Expr::PutValueSet { target, key, value, receiver, strict } => { tag(h, 12235); target.as_ref().hash(h); key.as_ref().hash(h); value.as_ref().hash(h); receiver.as_ref().hash(h); strict.hash(h); } Expr::WithGet { object, property, fallback } => { tag(h, 12236); object.as_ref().hash(h); property.hash(h); fallback.as_ref().hash(h); } Expr::WithSet { object, property, value, fallback, strict } => { tag(h, 12237); object.as_ref().hash(h); property.hash(h); value.as_ref().hash(h); hash_with_set_fallback(h, fallback); strict.hash(h); } diff --git a/crates/perry-hir/src/walker/expr_mut.rs b/crates/perry-hir/src/walker/expr_mut.rs index 656cc987fa..d5364617a1 100644 --- a/crates/perry-hir/src/walker/expr_mut.rs +++ b/crates/perry-hir/src/walker/expr_mut.rs @@ -1708,10 +1708,16 @@ where f(target); f(key); } - Expr::ReflectSet { target, key, value } => { + Expr::ReflectSet { + target, + key, + value, + receiver, + } => { f(target); f(key); f(value); + f(receiver); } Expr::PutValueSet { target, diff --git a/crates/perry-hir/src/walker/expr_ref.rs b/crates/perry-hir/src/walker/expr_ref.rs index bb37280470..a1280d98a9 100644 --- a/crates/perry-hir/src/walker/expr_ref.rs +++ b/crates/perry-hir/src/walker/expr_ref.rs @@ -1679,10 +1679,16 @@ where f(target); f(key); } - Expr::ReflectSet { target, key, value } => { + Expr::ReflectSet { + target, + key, + value, + receiver, + } => { f(target); f(key); f(value); + f(receiver); } Expr::PutValueSet { target, diff --git a/crates/perry-runtime/src/array/iter_methods.rs b/crates/perry-runtime/src/array/iter_methods.rs index 85cd052fea..261537b16c 100644 --- a/crates/perry-runtime/src/array/iter_methods.rs +++ b/crates/perry-runtime/src/array/iter_methods.rs @@ -867,6 +867,11 @@ pub extern "C" fn js_array_join_value( let separator = if separator_value.to_bits() == crate::value::TAG_UNDEFINED { ptr::null() } else { + // `ToString(separator)`: a Symbol separator throws a TypeError + // (§7.1.17) instead of rendering as "Symbol(…)". + if unsafe { crate::symbol::js_is_symbol(separator_value) } != 0 { + crate::collection_iter::throw_type_error("Cannot convert a Symbol value to a string"); + } crate::value::js_jsvalue_to_string(separator_value) as *const crate::string::StringHeader }; js_array_join(arr, separator) diff --git a/crates/perry-runtime/src/array/search.rs b/crates/perry-runtime/src/array/search.rs index caaca2181d..a515d5849f 100644 --- a/crates/perry-runtime/src/array/search.rs +++ b/crates/perry-runtime/src/array/search.rs @@ -119,14 +119,24 @@ pub extern "C" fn js_array_indexOf_jsvalue( if arr.is_null() { return -1; } - // TypedArray: strict-equality numeric search over the typed store. + // TypedArray: strict-equality search over the typed store. `len == 0` + // returns BEFORE ToIntegerOrInfinity(fromIndex) per spec (an observable / + // throwing coercion never runs on an empty array). `js_jsvalue_equals` + // (not `==`) so BigInt elements compare by value — `js_typed_array_get` + // on a BigInt64Array returns a freshly boxed BigInt whose bit pattern + // never raw-equals the search value. if let Some((ta, len)) = as_typed_array(arr) { + if len == 0 { + return -1; + } let start = match forward_start_index(len as i64, from_index, has_from) { Some(s) => s as i32, None => return -1, }; for i in start..len { - if crate::typedarray::js_typed_array_get(ta, i) == value { + if crate::value::js_jsvalue_equals(crate::typedarray::js_typed_array_get(ta, i), value) + == 1 + { return i; } } @@ -206,7 +216,11 @@ pub extern "C" fn js_array_last_index_of_jsvalue( }; let mut i = start; while i >= 0 { - if crate::typedarray::js_typed_array_get(ta, i as i32) == value { + if crate::value::js_jsvalue_equals( + crate::typedarray::js_typed_array_get(ta, i as i32), + value, + ) == 1 + { return i as i32; } i -= 1; @@ -295,6 +309,9 @@ pub extern "C" fn js_array_includes_jsvalue( // TypedArray: SameValueZero numeric search (so includes(NaN) is true for // float typed arrays). if let Some((ta, len)) = as_typed_array(arr) { + if len == 0 { + return 0; + } let start = match forward_start_index(len as i64, from_index, has_from) { Some(s) => s as i32, None => return 0, @@ -309,6 +326,11 @@ pub extern "C" fn js_array_includes_jsvalue( } unsafe { let length = (*arr).length as i64; + // §23.1.3.16 step 3: `len == 0 → false` BEFORE ToIntegerOrInfinity + // (fromIndex), mirroring `indexOf`. + if length == 0 { + return 0; + } let start = match forward_start_index(length, from_index, has_from) { Some(s) => s, None => return 0, diff --git a/crates/perry-runtime/src/builtins/numbers.rs b/crates/perry-runtime/src/builtins/numbers.rs index d1484b6302..8688bf9967 100644 --- a/crates/perry-runtime/src/builtins/numbers.rs +++ b/crates/perry-runtime/src/builtins/numbers.rs @@ -491,6 +491,19 @@ pub extern "C" fn js_number_coerce(value: f64) -> f64 { let joined = unsafe { crate::array::js_array_join(arr_ptr, comma) }; return js_number_coerce(crate::value::js_nanbox_string(joined as i64)); } + // TypedArray → OrdinaryToPrimitive(number): a *patched own* + // `valueOf`/`toString` expando (stored in the typed-array own-props + // side table, invisible to the generic object helpers below) runs + // first, with `this` = the typed array and abrupt completions + // propagating (test262 ctors/object-arg/throws-setting-obj-*). With + // no patch, fall through to the generic path (join + ToNumber). + if crate::typedarray::lookup_typed_array_kind(id as usize).is_some() { + if let Some(p) = unsafe { + crate::typedarray_props::typed_array_own_to_primitive_number(id as usize, value) + } { + return js_number_coerce(p); + } + } // Object → consult [Symbol.toPrimitive]("number") first; if the // object has a custom toPrimitive method, recurse with the result. let primitive = unsafe { crate::symbol::js_to_primitive(value, 1) }; diff --git a/crates/perry-runtime/src/object/field_get_set.rs b/crates/perry-runtime/src/object/field_get_set.rs index b34167206f..48fd3b1157 100644 --- a/crates/perry-runtime/src/object/field_get_set.rs +++ b/crates/perry-runtime/src/object/field_get_set.rs @@ -2236,8 +2236,12 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { if key_val.is_any_string() { let key_str = crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; + // `in` is [[HasProperty]], not [[HasOwnProperty]] — ordinary + // keys consult the prototype chain (`"subarray" in ta`, + // inherited `Object.prototype` expandos), while canonical + // numeric indices stay bounds-only. let present = - unsafe { crate::typedarray_props::typed_array_has_own_property(ta, key_str) }; + unsafe { crate::typedarray_props::typed_array_has_property(ta, key_str) }; return if present { nanbox_true } else { nanbox_false }; } if key_val.is_int32() { @@ -2710,6 +2714,18 @@ pub extern "C" fn js_object_get_field_by_name( } } } + // A user patch on the per-kind prototype + // (`Object.defineProperty(TA.prototype, + // "constructor", { get })` or a data overwrite) + // shadows the intrinsic — run the getter with + // `this` = the view (observable; test262 + // speciesctor-get-ctor-inherited reads + // `result.constructor` and counts calls). + if let Some(v) = + crate::typedarray::species::prototype_constructor_patch(kind, addr) + { + return JSValue::from_bits(v.to_bits()); + } let name = crate::typedarray::name_for_kind(kind); let ctor = super::js_get_global_this_builtin_value(name.as_ptr(), name.len()); @@ -2735,8 +2751,21 @@ pub extern "C" fn js_object_get_field_by_name( } b"BYTES_PER_ELEMENT" => return JSValue::number(1.0), b"constructor" => { + // An ArrayBuffer / SharedArrayBuffer cell answers + // with ITS constructor — only the Uint8Array + // (Buffer-backed view) representation reports + // `Uint8Array` (`ta.buffer.constructor === + // ArrayBuffer`, test262 ctors/buffer-arg/ + // typedarray-backed-by-sharedarraybuffer). + let name: &[u8] = if crate::buffer::is_shared_array_buffer(addr) { + b"SharedArrayBuffer" + } else if crate::buffer::is_any_array_buffer(addr) { + b"ArrayBuffer" + } else { + b"Uint8Array" + }; let ctor = - super::js_get_global_this_builtin_value(b"Uint8Array".as_ptr(), 10); + super::js_get_global_this_builtin_value(name.as_ptr(), name.len()); return JSValue::from_bits(ctor.to_bits()); } _ => {} @@ -3589,6 +3618,22 @@ pub extern "C" fn js_object_get_field_by_name( let ctor = super::js_get_global_this_builtin_value(b"DataView".as_ptr(), 8); return JSValue::from_bits(ctor.to_bits()); } + // An ArrayBuffer / SharedArrayBuffer answers with ITS + // constructor (`ta.buffer.constructor === ArrayBuffer`, + // test262 ctors/buffer-arg/typedarray-backed-by- + // sharedarraybuffer). + if crate::buffer::is_shared_array_buffer(obj as usize) { + let ctor = super::js_get_global_this_builtin_value( + b"SharedArrayBuffer".as_ptr(), + 17, + ); + return JSValue::from_bits(ctor.to_bits()); + } + if crate::buffer::is_any_array_buffer(obj as usize) { + let ctor = + super::js_get_global_this_builtin_value(b"ArrayBuffer".as_ptr(), 11); + return JSValue::from_bits(ctor.to_bits()); + } if crate::buffer::is_uint8array_buffer(obj as usize) { let ctor = super::js_get_global_this_builtin_value(b"Uint8Array".as_ptr(), 10); diff --git a/crates/perry-runtime/src/object/global_this.rs b/crates/perry-runtime/src/object/global_this.rs index 91d35878bf..d8e7c9747c 100644 --- a/crates/perry-runtime/src/object/global_this.rs +++ b/crates/perry-runtime/src/object/global_this.rs @@ -5401,17 +5401,64 @@ extern "C" fn typed_array_from_thunk( if kind_opt.is_none() { require_typed_array_from_of_constructor(); } - // Past the constructor check, the source is read — its `@@iterator` invoked, - // or its `length` getter + indexed elements evaluated — and any throwing - // user iterator/getter propagates. The map callback is validated up front by - // `js_array_from_mapped`. + // Spec order: validate the map callback BEFORE the source is read. let mapped = map_fn.to_bits() != crate::value::TAG_UNDEFINED; - let arr = if mapped { - crate::array::js_array_from_mapped(source, map_fn, this_arg) + let map_closure = if mapped { + crate::array::js_validate_array_callback(map_fn) as *const crate::closure::ClosureHeader } else { - crate::array::js_array_from_value(source) + std::ptr::null() }; - typed_array_create_from_values(kind_opt, arr) + // Read the source's RAW kValues — its `@@iterator` invoked, or its + // `ToLength(length)` + indexed elements evaluated — any throwing user + // iterator/getter propagates (test262 from/arylk-*-error). + let raw = unsafe { crate::typedarray::typed_array_from_source_raw_values(source) }; + // Per-element `mappedValue = Call(mapfn, T, «kValue, k»)` then + // `Set(target, k, mappedValue)` — the map call and the (observable, + // possibly throwing) element coercion INTERLEAVE per spec, so an abrupt + // coercion at element k means the map callback never ran for k+1 + // (test262 from/set-value-abrupt-completion). + let map_at = |k: usize, v: f64| -> f64 { + if map_closure.is_null() { + return v; + } + let prev = crate::object::js_implicit_this_set(this_arg); + let r = crate::closure::js_closure_call2(map_closure, v, k as f64); + crate::object::js_implicit_this_set(prev); + r + }; + if let Some(kind) = kind_opt { + let out = crate::typedarray::typed_array_alloc(kind, raw.len() as u32); + for (k, &v) in raw.iter().enumerate() { + let m = map_at(k, v); + unsafe { crate::typedarray_props::species_result_store(out as usize, k, m) }; + } + return crate::value::js_nanbox_pointer(out as i64); + } + // Custom `this` constructor: TypedArrayCreate(C, «len») then per-element + // [[Set]] (same interleave). + let len = raw.len(); + let len_arg = [f64::from_bits( + crate::value::JSValue::number(len as f64).bits(), + )]; + let ctor = crate::object::js_implicit_this_get(); + let target = unsafe { super::js_new_function_construct(ctor, len_arg.as_ptr(), 1) }; + let addr = crate::typedarray_props::typed_array_addr_from_value(target).unwrap_or_else(|| { + super::object_ops::throw_object_type_error( + b"TypedArray.from/of constructor did not return a TypedArray", + ) + }); + let ta_ptr = addr as *mut crate::typedarray::TypedArrayHeader; + let target_len = unsafe { crate::typedarray::js_typed_array_length(ta_ptr) } as usize; + if target_len < len { + super::object_ops::throw_object_type_error( + b"Derived TypedArray constructor created an array which was too small", + ); + } + for (k, &v) in raw.iter().enumerate() { + let m = map_at(k, v); + unsafe { crate::typedarray_props::species_result_store(addr, k, m) }; + } + target } /// `%TypedArray%.from`/`.of` step "If IsConstructor(`this`) is false, throw a diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index cd08a63088..4d9039d672 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -74,6 +74,45 @@ unsafe fn call_primitive_builtin_prototype_method( call_primitive_closure_value(receiver, value, args_ptr, args_len) } +/// A *user-installed* method on a builtin's prototype object (e.g. +/// `Number.prototype.toLocaleString = function () { … }`). Returns the patched +/// closure value, or `None` when the property is absent / not a real closure / +/// the no-op-backed builtin placeholder — i.e. `None` means "the native +/// builtin behavior is still in effect". +unsafe fn builtin_proto_user_method(builtin_name: &[u8], method_name: &str) -> Option { + let ctor = + crate::object::js_get_global_this_builtin_value(builtin_name.as_ptr(), builtin_name.len()); + let ctor_value = JSValue::from_bits(ctor.to_bits()); + if !ctor_value.is_pointer() { + return None; + } + let ctor_ptr = ctor_value.as_pointer::() as usize; + let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); + let proto_value = JSValue::from_bits(proto.to_bits()); + if !proto_value.is_pointer() { + return None; + } + let proto_ptr = proto_value.as_pointer::(); + if proto_ptr.is_null() { + return None; + } + let key = crate::string::js_string_from_bytes(method_name.as_ptr(), method_name.len() as u32); + let value = js_object_get_field_by_name(proto_ptr, key); + if (value.bits() & crate::value::TAG_MASK) != crate::value::POINTER_TAG { + return None; + } + let ptr = (value.bits() & crate::value::POINTER_MASK) as usize; + if !crate::closure::is_closure_ptr(ptr) { + return None; + } + if (*(ptr as *const crate::closure::ClosureHeader)).func_ptr + == super::global_this::global_this_builtin_noop_thunk as *const u8 + { + return None; + } + Some(value) +} + /// Call a method on an object with dynamic dispatch /// This is used for runtime method calls when the method cannot be resolved statically. /// object: NaN-boxed f64 containing an object pointer @@ -704,6 +743,23 @@ pub(crate) unsafe fn js_object_default_to_locale_string(receiver: f64) -> f64 { 0, ); } + // An own `toLocaleString` closure wins over the default rendering — + // notably `%TypedArray%.prototype.toLocaleString()` invoked as a method ON + // the prototype object itself must run the installed brand-check thunk + // (which throws for the non-TypedArray receiver, test262 + // toLocaleString/invoked-as-method). + { + let own = crate::object::js_object_get_own_field_or_undef( + receiver, + b"toLocaleString".as_ptr(), + 14, + ); + let own_value = JSValue::from_bits(own.to_bits()); + if let Some(result) = call_primitive_closure_value(receiver, own_value, std::ptr::null(), 0) + { + return result; + } + } if let Some(result) = call_object_to_string_method(receiver) { return result; } @@ -923,7 +979,7 @@ pub(super) unsafe fn dispatch_typed_array_method( } else { crate::typedarray::js_typed_array_sort_with_comparator(ta, cmp) }; - f64::from_bits(result as u64) + f64::from_bits(JSValue::pointer(result as *mut u8).bits()) } "toSorted" => { let cmp = if args_len >= 1 && !args_ptr.is_null() { @@ -937,9 +993,11 @@ pub(super) unsafe fn dispatch_typed_array_method( } else { crate::typedarray::js_typed_array_to_sorted_with_comparator(ta, cmp) }; - f64::from_bits(result as u64) + f64::from_bits(JSValue::pointer(result as *mut u8).bits()) } - "toReversed" => f64::from_bits(crate::typedarray::js_typed_array_to_reversed(ta) as u64), + "toReversed" => f64::from_bits( + JSValue::pointer(crate::typedarray::js_typed_array_to_reversed(ta) as *mut u8).bits(), + ), // #2879: bulk `set(source, offset?)` and `copyWithin`. "set" => { let source = arg0(); @@ -963,7 +1021,10 @@ pub(super) unsafe fn dispatch_typed_array_method( f64::from_bits(crate::value::TAG_UNDEFINED) }; f64::from_bits( - crate::typedarray::js_typed_array_copy_within(ta, target, start, end) as u64, + JSValue::pointer(crate::typedarray::js_typed_array_copy_within( + ta, target, start, end, + ) as *mut u8) + .bits(), ) } "with" => { @@ -973,7 +1034,10 @@ pub(super) unsafe fn dispatch_typed_array_method( } else { f64::NAN }; - f64::from_bits(crate::typedarray::js_typed_array_with(ta, idx, val) as u64) + f64::from_bits( + JSValue::pointer(crate::typedarray::js_typed_array_with(ta, idx, val) as *mut u8) + .bits(), + ) } "findLast" => crate::typedarray::js_typed_array_find_last(ta, validate_cb(false)), "findLastIndex" => { @@ -1044,7 +1108,14 @@ pub(super) unsafe fn dispatch_typed_array_method( // detect a registered TypedArray receiver and read its typed store, so a // `TypedArrayHeader*` cast to `ArrayHeader*` is sound here. "indexOf" | "lastIndexOf" | "includes" => { - let value = arg0(); + // Absent searchElement is `undefined`, NOT the NaN sentinel — + // `new Float64Array([NaN]).includes()` must be false (SameValueZero + // against undefined), and NaN never `===`-matches for indexOf. + let value = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; let (has_from, from) = if args_len >= 2 && !args_ptr.is_null() { (1, *args_ptr.add(1)) } else { @@ -1071,16 +1142,49 @@ pub(super) unsafe fn dispatch_typed_array_method( let s = crate::typedarray::js_typed_array_join_value(ta, sep); f64::from_bits(JSValue::string_ptr(s).bits()) } - // `%TypedArray%.prototype.toLocaleString` defaults to a comma-separated - // join (spec: each element's `toLocaleString`, joined by ","). Perry's - // numbers stringify identically here, so a default `join` matches Node - // for the common numeric cases the brand-check tests exercise. + // `%TypedArray%.prototype.toLocaleString` (§23.2.3.32): for each + // element, `? ToString(? Invoke(element, "toLocaleString"))`, joined by + // ",". When the user has NOT replaced `Number.prototype.toLocaleString` + // (or `BigInt.prototype...` for the bigint kinds) the result is the + // default comma-separated join, which Perry's plain `join` matches — + // keep that fast path. With a patch installed, run the spec loop so + // the user function is invoked per element (its result then goes + // through ordinary ToString, running `toString`/`valueOf` and + // propagating abrupt completions). "toLocaleString" => { - let s = crate::typedarray::js_typed_array_join_value( - ta, - f64::from_bits(crate::value::TAG_UNDEFINED), + let kind = crate::typedarray::lookup_typed_array_kind(ta as usize); + let is_bigint = matches!( + kind, + Some(crate::typedarray::KIND_BIGINT64) | Some(crate::typedarray::KIND_BIGUINT64) ); - f64::from_bits(JSValue::string_ptr(s).bits()) + let builtin: &[u8] = if is_bigint { b"BigInt" } else { b"Number" }; + match builtin_proto_user_method(builtin, "toLocaleString") { + None => { + let s = crate::typedarray::js_typed_array_join_value( + ta, + f64::from_bits(crate::value::TAG_UNDEFINED), + ); + f64::from_bits(JSValue::string_ptr(s).bits()) + } + Some(patched) => { + let len = crate::typedarray::js_typed_array_length(ta); + let mut out = String::new(); + for k in 0..len { + if k > 0 { + out.push(','); + } + let elem = crate::typedarray::js_typed_array_get(ta, k); + let r = call_primitive_closure_value(elem, patched, std::ptr::null(), 0) + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + let s_hdr = crate::builtins::js_string_coerce(r); + out.push_str( + super::has_own_helpers::str_from_string_header(s_hdr).unwrap_or(""), + ); + } + let s = crate::string::js_string_from_bytes(out.as_ptr(), out.len() as u32); + f64::from_bits(JSValue::string_ptr(s).bits()) + } + } } "slice" => { // `ToIntegerOrInfinity` each index (runs `valueOf`/`Symbol.toPrimitive`, diff --git a/crates/perry-runtime/src/object/object_ops.rs b/crates/perry-runtime/src/object/object_ops.rs index 4256e91e0e..68a5f47358 100644 --- a/crates/perry-runtime/src/object/object_ops.rs +++ b/crates/perry-runtime/src/object/object_ops.rs @@ -1470,6 +1470,64 @@ pub extern "C" fn js_object_define_property( } if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(obj_value) { + // A Symbol key on a TypedArray is an ORDINARY define — store it in + // the symbol side tables (string-coercing it would file the value + // under a "Symbol(x)" string name, unreachable via `ta[sym]`), + // honoring accessor descriptors and recording the attributes + // (defineProperty defaults absent fields to false, unlike a plain + // `ta[sym] = v` write). Mirrors the generic symbol-define block. + if crate::symbol::js_is_symbol(key_value) != 0 { + let desc_ptr = extract_obj_ptr(descriptor_value); + if desc_ptr.is_null() { + return obj_value; + } + let has_get = desc_has_field(descriptor_value, b"get"); + let has_set = desc_has_field(descriptor_value, b"set"); + let has_accessor = has_get || has_set; + if has_accessor { + let get_field = desc_read_field(descriptor_value, b"get"); + let set_field = desc_read_field(descriptor_value, b"set"); + let get_bits = if !has_get || get_field.is_undefined() { + 0 + } else { + crate::closure::clone_closure_rebind_this(get_field.bits(), obj_value) + }; + let set_bits = if !has_set || set_field.is_undefined() { + 0 + } else { + crate::closure::clone_closure_rebind_this(set_field.bits(), obj_value) + }; + crate::symbol::set_symbol_accessor_property( + obj_value, key_value, get_bits, set_bits, + ); + } else { + let value_field = desc_read_field(descriptor_value, b"value"); + crate::symbol::js_object_set_symbol_property( + obj_value, + key_value, + f64::from_bits(value_field.bits()), + ); + } + let read_flag = |name: &[u8]| -> Option { + if !desc_has_field(descriptor_value, name) { + return None; + } + let v = desc_read_field(descriptor_value, name); + Some(crate::value::js_is_truthy(f64::from_bits(v.bits())) != 0) + }; + let owner = crate::symbol::obj_key_from_f64(obj_value); + let sym_key = crate::symbol::sym_key_from_f64(key_value); + crate::symbol::set_symbol_property_attrs( + owner, + sym_key, + PropertyAttrs::new( + read_flag(b"writable").unwrap_or(has_accessor), + read_flag(b"enumerable").unwrap_or(false), + read_flag(b"configurable").unwrap_or(false), + ), + ); + return obj_value; + } let key_str = crate::builtins::js_string_coerce(key_value); if key_str.is_null() { return obj_value; diff --git a/crates/perry-runtime/src/object/object_ops_frozen.rs b/crates/perry-runtime/src/object/object_ops_frozen.rs index a50f97f65e..62356b84a7 100644 --- a/crates/perry-runtime/src/object/object_ops_frozen.rs +++ b/crates/perry-runtime/src/object/object_ops_frozen.rs @@ -306,6 +306,13 @@ pub extern "C" fn js_object_prevent_extensions(obj_value: f64) -> f64 { unsafe { let obj = extract_obj_ptr(obj_value); if !obj.is_null() && (obj as usize) > 0x10000 { + // Typed arrays: side table, NOT the GC header — small typed + // arrays are plain-`alloc`ed with no `GcHeader`, so the flag + // write below would corrupt allocator metadata. + if crate::typedarray::lookup_typed_array_kind(obj as usize).is_some() { + crate::typedarray_props::typed_array_mark_no_extend(obj as usize); + return obj_value; + } let gc = gc_header_for(obj); (*gc)._reserved |= crate::gc::OBJ_FLAG_NO_EXTEND; } @@ -557,10 +564,14 @@ pub extern "C" fn js_object_is_extensible(obj_value: f64) -> f64 { // objects are extensible by default; report that instead of reading a // header that may not exist. let raw = crate::value::js_nanbox_get_pointer(obj_value) as usize; - if raw > 0x10000 - && (crate::typedarray::lookup_typed_array_kind(raw).is_some() - || crate::buffer::is_registered_buffer(raw)) - { + if raw > 0x10000 && crate::typedarray::lookup_typed_array_kind(raw).is_some() { + return if crate::typedarray_props::typed_array_owner_no_extend(raw) { + f64::from_bits(TAG_FALSE) + } else { + f64::from_bits(TAG_TRUE) + }; + } + if raw > 0x10000 && crate::buffer::is_registered_buffer(raw) { return f64::from_bits(TAG_TRUE); } let gc = gc_header_for(obj); diff --git a/crates/perry-runtime/src/object/reflect_support.rs b/crates/perry-runtime/src/object/reflect_support.rs index 61ea758d0d..018ae51f2e 100644 --- a/crates/perry-runtime/src/object/reflect_support.rs +++ b/crates/perry-runtime/src/object/reflect_support.rs @@ -26,6 +26,11 @@ pub(crate) fn obj_value_no_extend(value: f64) -> bool { if obj.is_null() || (obj as usize) <= 0x10000 { return false; } + // Typed arrays use a side table (small ones carry no `GcHeader`, so + // the header read below would be allocator-metadata garbage). + if crate::typedarray::lookup_typed_array_kind(obj as usize).is_some() { + return crate::typedarray_props::typed_array_owner_no_extend(obj as usize); + } let gc = gc_header_for(obj); (*gc)._reserved & crate::gc::OBJ_FLAG_NO_EXTEND != 0 } @@ -46,6 +51,20 @@ pub(crate) fn obj_value_has_own_key(value: f64, key: f64) -> bool { return false; } let obj_addr = obj as usize; + // TypedArray FIRST: own keys are the valid integer indices plus the + // expando side table. Must precede the GC-header read below — small + // typed arrays are plain-`alloc`ed without a `GcHeader`, so reading + // `addr - 8` is allocator-metadata garbage. + if crate::typedarray::lookup_typed_array_kind(obj_addr).is_some() { + let key_str = crate::builtins::js_string_coerce(key); + if key_str.is_null() { + return false; + } + return crate::typedarray_props::typed_array_has_own_property( + obj as *const crate::typedarray::TypedArrayHeader, + key_str, + ); + } if obj_addr >= crate::gc::GC_HEADER_SIZE + 0x1000 { let gc = gc_header_for(obj); if (*gc).obj_type == crate::gc::GC_TYPE_ARRAY diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index 3eb4ed390f..234bc6569d 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -759,6 +759,22 @@ fn reflect_ordinary_set_property_key(target: f64, property_key: f64, value: f64) )) } +/// `Reflect.set` with an explicit receiver: OrdinarySet(target, P, V, +/// receiver), boolean result NaN-boxed. +pub(crate) fn reflect_ordinary_set_with_receiver( + target: f64, + property_key: f64, + value: f64, + receiver: f64, +) -> f64 { + nanbox_bool(ordinary_set_with_receiver( + target, + property_key, + value, + receiver, + )) +} + fn reflect_ordinary_set(target: f64, key: f64, value: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let target_handle = scope.root_nanbox_f64(target); @@ -1093,6 +1109,58 @@ fn ordinary_set_with_receiver(target: f64, key: f64, value: f64, receiver: f64) let mut current = target; for _ in 0..64 { + // Integer-Indexed exotic [[Set]] (§10.4.5.5): a typed array in the + // chain intercepts a canonical numeric index key — the prototype + // chain is NEVER consulted for it. `SameValue(O, Receiver)` writes + // the element; a different receiver with a valid index falls to the + // ordinary data-descriptor flow (create on receiver); an invalid + // canonical index is a silent no-op `true`. + let cur_addr = extract_pointer(current.to_bits()) as usize; + if crate::typedarray::lookup_typed_array_kind(cur_addr).is_some() { + if let Some(name) = property_key_to_rust_string(key) { + match crate::typedarray_props::typed_array_canonical_index_validity(cur_addr, &name) + { + Some(valid) => { + let recv_addr = extract_pointer(receiver.to_bits()) as usize; + if recv_addr == cur_addr { + return unsafe { + crate::typedarray_props::typed_array_set_property_by_name( + cur_addr, &name, value, + ) + }; + } + if !valid { + return true; + } + // The receiver may itself be a typed array: the + // CreateDataProperty lands in ITS [[DefineOwnProperty]], + // which rejects an index that is invalid FOR THE + // RECEIVER (`Reflect.set(ta, "0", v, emptyTa)` → false). + if crate::typedarray::lookup_typed_array_kind(recv_addr).is_some() { + return match crate::typedarray_props:: + typed_array_canonical_index_validity(recv_addr, &name) + { + Some(true) => unsafe { + crate::typedarray_props::typed_array_set_property_by_name( + recv_addr, &name, value, + ) + }, + Some(false) => false, + None => create_or_update_receiver_property(receiver, key, value), + }; + } + return create_or_update_receiver_property(receiver, key, value); + } + // Ordinary key on a TA in the chain: stop the walk (Perry's + // TA prototype methods are served natively, not as data + // descriptors visible to `own_set_descriptor`) and define + // on the receiver. + None => { + return create_or_update_receiver_property(receiver, key, value); + } + } + } + } if let Some(desc) = own_set_descriptor(current, key) { return match desc { OwnSetDescriptor::Data { writable } => { diff --git a/crates/perry-runtime/src/proxy/reflect.rs b/crates/perry-runtime/src/proxy/reflect.rs index dfc313857a..9725b9294c 100644 --- a/crates/perry-runtime/src/proxy/reflect.rs +++ b/crates/perry-runtime/src/proxy/reflect.rs @@ -2,7 +2,7 @@ use super::{ closure_from, coerce_trap_bool, extract_pointer, handler_trap, is_callable_function, js_closure_call0, js_closure_call2, js_proxy_delete, js_proxy_get, js_proxy_has, js_proxy_set, lookup, nanbox_bool, reflect_non_object_typeerror, reflect_ordinary_delete_property_key, - reflect_ordinary_set_property_key, reflect_value_is_object, revoked_return, + reflect_ordinary_set_with_receiver, reflect_value_is_object, revoked_return, target_get_property_key, throw_type_error, PROXIES, TAG_NULL, TAG_TRUE, TAG_UNDEFINED, }; @@ -75,11 +75,14 @@ pub extern "C" fn js_reflect_get(target: f64, key: f64, receiver: f64) -> f64 { result } -/// `Reflect.set(target, key, value)` - returns the boolean result of the -/// `[[Set]]` operation (#2756): `false` for a non-writable property or a new -/// key on a non-extensible object, and the coerced trap result for a proxy. +/// `Reflect.set(target, key, value, receiver?)` - returns the boolean result +/// of the `[[Set]]` operation (#2756): `false` for a non-writable property or +/// a new key on a non-extensible object, and the coerced trap result for a +/// proxy. An absent/`undefined` `receiver` defaults to `target`; a distinct +/// receiver redirects the eventual data write per OrdinarySet (observable for +/// Integer-Indexed exotic targets — test262 internals/Set/*-reflect-set). #[no_mangle] -pub extern "C" fn js_reflect_set(target: f64, key: f64, value: f64) -> f64 { +pub extern "C" fn js_reflect_set(target: f64, key: f64, value: f64, receiver: f64) -> f64 { // Reflect.set on a non-object target must throw TypeError (spec step 1), // matching Reflect.has/get/etc. Pre-fix it silently returned false. if !reflect_value_is_object(target) { @@ -89,15 +92,24 @@ pub extern "C" fn js_reflect_set(target: f64, key: f64, value: f64) -> f64 { let target_handle = scope.root_nanbox_f64(target); let key_handle = scope.root_nanbox_f64(key); let value_handle = scope.root_nanbox_f64(value); + let receiver_handle = scope.root_nanbox_f64(receiver); let property_key_handle = scope .root_nanbox_f64(unsafe { crate::object::js_to_property_key(key_handle.get_nanbox_f64()) }); let target = target_handle.get_nanbox_f64(); let property_key = property_key_handle.get_nanbox_f64(); let value = value_handle.get_nanbox_f64(); + let receiver = { + let r = receiver_handle.get_nanbox_f64(); + if r.to_bits() == crate::value::TAG_UNDEFINED { + target + } else { + r + } + }; if lookup(target).is_some() { return js_proxy_set(target, property_key, value); } - reflect_ordinary_set_property_key(target, property_key, value) + reflect_ordinary_set_with_receiver(target, property_key, value, receiver) } /// `Reflect.has(target, key)` (#2764) - `[[HasProperty]]` semantics: diff --git a/crates/perry-runtime/src/typedarray/bigint.rs b/crates/perry-runtime/src/typedarray/bigint.rs index 7b90d4fdac..71783b298d 100644 --- a/crates/perry-runtime/src/typedarray/bigint.rs +++ b/crates/perry-runtime/src/typedarray/bigint.rs @@ -112,6 +112,17 @@ pub(crate) fn to_bigint_for_store(value: f64) -> f64 { // object — e.g. `bigIntView.fill({ valueOf() { return 7n } })` — must run its // coercion hook rather than throwing "Cannot convert NaN to a BigInt". if jsval.is_pointer() && unsafe { crate::symbol::js_is_symbol(value) } == 0 { + // A typed-array source with a *patched own* `valueOf`/`toString` + // expando runs it first (test262 ctors-bigint/object-arg/ + // throws-setting-obj-* — the patch may throw, observably). + let addr = (value.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; + if crate::typedarray::lookup_typed_array_kind(addr).is_some() { + if let Some(p) = + unsafe { crate::typedarray_props::typed_array_own_to_primitive_number(addr, value) } + { + return to_bigint_for_store(p); + } + } match unsafe { crate::value::to_primitive_number(value) } { crate::value::OrdinaryToPrimitiveOutcome::Primitive(p) if p.to_bits() != value.to_bits() => diff --git a/crates/perry-runtime/src/typedarray/mod.rs b/crates/perry-runtime/src/typedarray/mod.rs index 07497acf24..c7f23fb826 100644 --- a/crates/perry-runtime/src/typedarray/mod.rs +++ b/crates/perry-runtime/src/typedarray/mod.rs @@ -166,6 +166,7 @@ pub fn unregister_typed_array(ptr: *const TypedArrayHeader) { }); crate::typedarray_view::clear_view_meta(owner); crate::typedarray_props::typed_array_clear_own_props(owner); + crate::typedarray_props::typed_array_clear_no_extend(owner); } /// Returns Some(kind) if the (already-stripped) address is a registered @@ -549,7 +550,7 @@ pub fn typed_array_alloc(kind: u8, length: u32) -> *mut TypedArrayHeader { /// Convert an f64 (NaN-boxed JS value) to the numeric value to store. Strings /// and undefined become 0/NaN. -fn jsvalue_to_f64(v: f64) -> f64 { +pub(crate) fn jsvalue_to_f64(v: f64) -> f64 { let bits = v.to_bits(); let top16 = bits >> 48; // Plain double — positive, negative, ±Inf, and all NaN patterns that @@ -822,20 +823,35 @@ pub extern "C" fn js_typed_array_new(kind: i32, val: f64) -> *mut TypedArrayHead ); } // A plain object that is neither a typed array nor a buffer is consumed - // per the spec's `new TypedArray(object)` path: if it exposes - // `@@iterator` it is iterated (InitializeTypedArrayFromList), otherwise - // it is read as an array-like (`ToLength(obj.length)` then each indexed - // element). Previously the object pointer was reinterpreted as an - // `ArrayHeader`, so `obj.length` was read from the wrong header (garbage, - // usually 1) and the elements were raw bytes. Route through the shared - // `Array.from` materialization (which performs exactly this dual - // iterator/array-like resolution and propagates any user getter/iterator - // exceptions), then coerce each element to the per-kind numeric type. + // per the spec's `new TypedArray(object)` path: if it exposes a + // *callable* `@@iterator` it is iterated (InitializeTypedArrayFromList); + // a non-callable non-nullish `@@iterator` is a TypeError; otherwise it + // is read as an array-like (`ToLength(Get(obj, "length"))` then each + // indexed element). Registered Maps/Sets keep the shared `Array.from` + // materialization (their `@@iterator` is native, not a stored symbol + // property). Functions are valid array-like/iterable sources too — + // previously they were reinterpreted as an `ArrayHeader` (crash). + if crate::map::is_registered_map(raw_addr) + || crate::set::is_registered_set(raw_addr) + || crate::array::is_builtin_iterator_class_id(raw_addr) + || crate::object::js_util_types_is_generator_object(val).to_bits() + == crate::value::TAG_TRUE + { + // Built-in iterables whose `@@iterator` is native (not a stored + // symbol property): Maps/Sets, builtin iterator objects, and + // generator objects (Perry generators carry own `next`/`return` + // closures and no `@@iterator` symbol prop). The shared + // `Array.from` materialization drives these correctly. + let materialized = crate::array::js_array_from_value(val); + return js_typed_array_new_from_array(kind, materialized); + } + if crate::closure::is_closure_ptr(raw_addr) { + return unsafe { typed_array_from_plain_object(kind as u8, val) }; + } if raw_addr >= crate::gc::GC_HEADER_SIZE + 0x1000 { let gc_hdr = (raw_addr - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; if unsafe { (*gc_hdr).obj_type } == crate::gc::GC_TYPE_OBJECT { - let materialized = crate::array::js_array_from_value(val); - return js_typed_array_new_from_array(kind, materialized); + return unsafe { typed_array_from_plain_object(kind as u8, val) }; } } return js_typed_array_new_from_array(kind, arr); @@ -868,8 +884,145 @@ pub extern "C" fn js_typed_array_new(kind: i32, val: f64) -> *mut TypedArrayHead let len = typed_array_length_or_throw(val); return typed_array_alloc(kind as u8, len); } - // Undefined / null / bool / string → empty typed array. - typed_array_alloc(kind as u8, 0) + // Undefined → ToIndex(undefined) = 0. Null / bool / string run through + // ToNumber then ToIndex, so `new TA(true)` and `new TA('1')` have length + // 1 (previously all of these built an empty array). + if bits == crate::value::TAG_UNDEFINED { + return typed_array_alloc(kind as u8, 0); + } + let len = typed_array_length_or_throw(jsvalue_to_f64(val)); + typed_array_alloc(kind as u8, len) +} + +/// `new TA(object)` for a plain object / function source (ES2024 §23.2.5.1 +/// step 6.b.iii, InitializeTypedArrayFromList / InitializeTypedArrayFromArrayLike). +/// +/// - `GetMethod(obj, @@iterator)`: a non-nullish, non-callable value is a +/// TypeError; a callable one drives the iterator protocol (each `next()` +/// may throw — propagate). +/// - Otherwise array-like: `len = ToLength(? Get(obj, "length"))` (a Symbol +/// length is a TypeError, a `valueOf` runs and may throw), then each +/// indexed element is read and coerced per kind (`ToNumber`/`ToBigInt`, +/// both observable / throwing). +/// +/// Element values are fully collected BEFORE coercion begins, mirroring the +/// snapshot rule in `js_typed_array_new_from_array`. +unsafe fn typed_array_from_plain_object(kind: u8, val: f64) -> *mut TypedArrayHeader { + let raw = typed_array_plain_object_values(val); + typed_array_from_snapshot(kind, raw) +} + +/// Collect the raw (uncoerced) element values of a plain-object / function +/// source per the spec's iterator-or-array-like resolution (see +/// `typed_array_from_plain_object` doc above). Observable: the `@@iterator` +/// validation/iteration, the `ToLength(Get(obj, "length"))` coercion, and +/// each indexed `Get` all run here and may throw. +unsafe fn typed_array_plain_object_values(val: f64) -> Vec { + let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); + let iter_wk = crate::symbol::well_known_symbol("iterator"); + let using_iter = if iter_wk.is_null() { + undefined + } else { + let sym = f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); + crate::symbol::js_object_get_symbol_property(val, sym) + }; + let ub = using_iter.to_bits(); + if ub != crate::value::TAG_UNDEFINED && ub != crate::value::TAG_NULL { + let fn_raw = crate::value::js_nanbox_get_pointer(using_iter) as usize; + if fn_raw < 0x10000 || !crate::closure::is_closure_ptr(fn_raw) { + throw_type_error(b"object is not iterable"); + } + let bound = crate::closure::clone_closure_rebind_this(using_iter.to_bits(), val); + let iter = crate::closure::js_native_call_value(f64::from_bits(bound), ptr::null(), 0); + let mut raw: Vec = Vec::new(); + while let Some(v) = crate::collection_iter::iterator_next_value(iter) { + raw.push(v); + } + return raw; + } + // Array-like path. + let len_val = object_like_get(val, "length"); + let n = jsvalue_to_f64(len_val); + // ToLength: NaN / negative → 0, clamp to 2^53-1. + let len = if n.is_nan() || n <= 0.0 { + 0.0 + } else { + n.trunc().min(9_007_199_254_740_991.0) + }; + // AllocateTypedArrayBuffer implementation limit (Node throws RangeError + // for lengths past the max typed-array size). + if len > u32::MAX as f64 { + throw_range_error(format!("Invalid typed array length: {}", len as u64).as_bytes()); + } + let len = len as u32; + let mut raw: Vec = Vec::with_capacity(len as usize); + for k in 0..len { + raw.push(object_like_get(val, &k.to_string())); + } + raw +} + +/// Collect the raw (uncoerced) source values for `%TypedArray%.from(source)`: +/// plain-object / function sources use the spec iterator-or-array-like +/// resolution (so a throwing `length` getter / `ToLength(Symbol)` / a +/// non-callable `@@iterator` propagate); every other shape (arrays, strings, +/// Maps, Sets, iterators, generators, buffers) goes through the shared +/// `Array.from` materialization. +pub(crate) unsafe fn typed_array_from_source_raw_values(val: f64) -> Vec { + let bits = val.to_bits(); + if (bits >> 48) == 0x7FFD { + let raw_addr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; + let special = crate::map::is_registered_map(raw_addr) + || crate::set::is_registered_set(raw_addr) + || crate::array::is_builtin_iterator_class_id(raw_addr) + || crate::object::js_util_types_is_generator_object(val).to_bits() + == crate::value::TAG_TRUE + || lookup_typed_array_kind(raw_addr).is_some() + || crate::buffer::is_registered_buffer(raw_addr) + || crate::symbol::js_is_symbol(val) != 0; + if !special { + if crate::closure::is_closure_ptr(raw_addr) { + return typed_array_plain_object_values(val); + } + if raw_addr >= crate::gc::GC_HEADER_SIZE + 0x1000 { + let gc_hdr = (raw_addr - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*gc_hdr).obj_type == crate::gc::GC_TYPE_OBJECT { + return typed_array_plain_object_values(val); + } + } + } + } + let arr = crate::array::js_array_from_value(val); + let len = crate::array::js_array_length(arr); + (0..len as u32) + .map(|i| crate::array::js_array_get_f64(arr, i)) + .collect() +} + +/// Coerce a snapshot of raw element values per `kind` (observable, may throw) +/// and store them into a freshly allocated typed array. +unsafe fn typed_array_from_snapshot(kind: u8, raw: Vec) -> *mut TypedArrayHeader { + let vals: Vec = raw + .into_iter() + .map(|v| bigint::coerce_for_kind(kind, v)) + .collect(); + let ta = typed_array_alloc(kind, vals.len() as u32); + for (i, v) in vals.iter().enumerate() { + store_at(ta, i, *v); + } + ta +} + +/// `Get(obj, name)` for a plain-object or function source value. +unsafe fn object_like_get(val: f64, name: &str) -> f64 { + let raw = crate::value::js_nanbox_get_pointer(val) as usize; + if crate::closure::is_closure_ptr(raw) { + return crate::closure::closure_get_dynamic_prop(raw, name); + } + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let v = + crate::object::js_object_get_field_by_name(raw as *const crate::object::ObjectHeader, key); + f64::from_bits(v.bits()) } /// Copy elements from one typed array into a new typed array of `dst_kind`, @@ -1103,6 +1256,32 @@ unsafe fn classify_set_source(source_value: f64, dst_kind: u8) -> Option> 48; let addr = if top16 == 0x7FFD { @@ -1396,6 +1575,46 @@ pub extern "C" fn js_typed_array_to_reversed(ta: *const TypedArrayHeader) -> *mu } } +/// Spec default sort order for typed-array Numbers (`%TypedArray%.prototype. +/// sort` without a comparator): ascending, every NaN at the end, and `-0` +/// before `+0`. `partial_cmp` got neither right (NaN compared `Equal` so NaNs +/// stayed in place; `-0 == +0` left zeros in input order). +fn typed_array_default_number_cmp(a: &f64, b: &f64) -> std::cmp::Ordering { + match (a.is_nan(), b.is_nan()) { + (true, true) => std::cmp::Ordering::Equal, + (true, false) => std::cmp::Ordering::Greater, + (false, true) => std::cmp::Ordering::Less, + _ => a.total_cmp(b), + } +} + +/// Default-sort `ta`'s elements in place. BigInt kinds sort the raw 64-bit +/// lanes (signed/unsigned) — `load_at` boxes each element as a fresh BigInt +/// pointer, and sorting those bit patterns scrambled the array. +unsafe fn typed_array_sort_default_in_place(ta: *mut TypedArrayHeader) { + let len = (*ta).length as usize; + if len <= 1 { + return; + } + match (*ta).kind { + KIND_BIGINT64 => { + let base = data_ptr_mut(ta) as *mut i64; + std::slice::from_raw_parts_mut(base, len).sort_unstable(); + } + KIND_BIGUINT64 => { + let base = data_ptr_mut(ta) as *mut u64; + std::slice::from_raw_parts_mut(base, len).sort_unstable(); + } + _ => { + let mut buf: Vec = (0..len).map(|i| load_at(ta, i)).collect(); + buf.sort_by(typed_array_default_number_cmp); + for (i, v) in buf.into_iter().enumerate() { + store_at(ta, i, v); + } + } + } +} + /// `ta.sort()` — default ascending numeric sort, **in place**. Per the /// JS spec, the same typed-array reference is returned. Issue #654. #[no_mangle] @@ -1405,15 +1624,7 @@ pub extern "C" fn js_typed_array_sort_default(ta: *mut TypedArrayHeader) -> *mut return ta_clean; } unsafe { - let len = (*ta_clean).length as usize; - if len <= 1 { - return ta_clean; - } - let mut buf: Vec = (0..len).map(|i| load_at(ta_clean, i)).collect(); - buf.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); - for (i, v) in buf.into_iter().enumerate() { - store_at(ta_clean, i, v); - } + typed_array_sort_default_in_place(ta_clean); ta_clean } } @@ -1468,12 +1679,11 @@ pub extern "C" fn js_typed_array_to_sorted_default( let kind = (*ta).kind; let len = (*ta).length as usize; let out = typed_array_alloc(kind, len as u32); - // Materialize values, sort, store back. - let mut buf: Vec = (0..len).map(|i| load_at(ta, i)).collect(); - buf.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); - for (i, v) in buf.into_iter().enumerate() { - store_at(out, i, v); - } + // Copy the raw lanes, then reuse the in-place default sort (BigInt + // kinds sort raw 64-bit lanes; Number kinds use the spec NaN/-0 order). + let elem = (*ta).elem_size as usize; + ptr::copy_nonoverlapping(data_ptr(ta), data_ptr_mut(out), len * elem); + typed_array_sort_default_in_place(out); out } } @@ -1938,6 +2148,11 @@ pub extern "C" fn js_typed_array_join_value( let separator = if separator_value.to_bits() == crate::value::TAG_UNDEFINED { ptr::null() } else { + // `ToString(separator)`: a Symbol separator is a TypeError (§7.1.17), + // not a "Symbol(…)" rendering. + if unsafe { crate::symbol::js_is_symbol(separator_value) } != 0 { + throw_type_error(b"Cannot convert a Symbol value to a string"); + } crate::value::js_jsvalue_to_string(separator_value) as *const crate::string::StringHeader }; js_typed_array_join(ta, separator) diff --git a/crates/perry-runtime/src/typedarray/species.rs b/crates/perry-runtime/src/typedarray/species.rs index edd99aadb2..67023ef0d7 100644 --- a/crates/perry-runtime/src/typedarray/species.rs +++ b/crates/perry-runtime/src/typedarray/species.rs @@ -51,17 +51,70 @@ pub(crate) unsafe fn species_constructor(owner: usize, kind: u8) -> SpeciesChoic } /// `Get(O, "constructor")` — an own expando (data or accessor, the latter -/// runs its getter) wins; otherwise resolves to the intrinsic constructor for -/// this element kind (the default `%Int8Array%` … `%Float64Array%`). +/// runs its getter) wins; next the *prototype object* is consulted, so a +/// user `Object.defineProperty(TA.prototype, "constructor", { get })` runs +/// its getter (observable — test262 speciesctor-get-ctor-inherited counts the +/// calls) and a data overwrite is honored; otherwise resolves to the intrinsic +/// constructor for this element kind (`%Int8Array%` … `%Float64Array%`). unsafe fn read_constructor(owner: usize, kind: u8) -> f64 { if let Some(v) = crate::typedarray_props::typed_array_get_property_value_by_name(owner, "constructor") { return v; } + if let Some(v) = prototype_constructor_patch(kind, owner) { + return v; + } intrinsic_constructor(kind) } +/// A user-patched `constructor` on this kind's prototype object +/// (`Float64Array.prototype` etc.). `js_object_get_field_by_name` runs any +/// accessor getter stored for the prototype object; an explicitly-patched +/// `undefined` result is meaningful (spec: `C === undefined` → default +/// constructor), so we distinguish "patched" by whether a descriptor or data +/// field for the key exists at all. +pub(crate) unsafe fn prototype_constructor_patch(kind: u8, owner: usize) -> Option { + let name = name_for_kind(kind); + let ctor = crate::object::js_get_global_this_builtin_value(name.as_ptr(), name.len()); + let cv = JSValue::from_bits(ctor.to_bits()); + if !cv.is_pointer() { + return None; + } + let ctor_ptr = crate::value::js_nanbox_get_pointer(ctor) as usize; + let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); + let pv = JSValue::from_bits(proto.to_bits()); + if !pv.is_pointer() { + return None; + } + let proto_ptr = crate::value::js_nanbox_get_pointer(proto) as *mut crate::object::ObjectHeader; + if proto_ptr.is_null() { + return None; + } + // Accessor descriptor on the prototype: run the getter with + // `this = owner` (observable; its return value — even `undefined` — is + // the spec's `C`). A get-less accessor reads as `undefined`. + if let Some(desc) = crate::object::get_accessor_descriptor(proto_ptr as usize, "constructor") { + if desc.get == 0 { + return Some(f64::from_bits(TAG_UNDEFINED)); + } + let owner_value = f64::from_bits(JSValue::pointer(owner as *const u8).bits()); + let bound = crate::closure::clone_closure_rebind_this(desc.get, owner_value); + return Some(crate::closure::js_native_call_value( + f64::from_bits(bound), + std::ptr::null(), + 0, + )); + } + // Plain data overwrite (`TA.prototype.constructor = X`). + let key = crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); + let v = crate::object::js_object_get_field_by_name(proto_ptr, key); + if v.is_undefined() { + return None; + } + Some(f64::from_bits(v.bits())) +} + /// The intrinsic constructor value for an element kind (`Uint8Array`, …). pub(crate) fn intrinsic_constructor(kind: u8) -> f64 { let name = name_for_kind(kind); diff --git a/crates/perry-runtime/src/typedarray_props.rs b/crates/perry-runtime/src/typedarray_props.rs index 93c809ed19..93f69104f5 100644 --- a/crates/perry-runtime/src/typedarray_props.rs +++ b/crates/perry-runtime/src/typedarray_props.rs @@ -175,11 +175,16 @@ fn is_canonical_numeric_index_name(name: &str) -> bool { if !value.is_finite() { return false; } - if value.fract() == 0.0 && value.abs() <= i64::MAX as f64 { - format!("{}", value as i64) == name - } else { - format!("{value}") == name + // CanonicalNumericIndexString requires `ToString(ToNumber(name)) == name` + // with the JS Number→String rendering — Rust's `format!` prints `1e21` as + // `1000000000000000000000` and `1e-7` as `0.0000001`, which wrongly + // classified those keys as canonical (JS renders `1e+21` / `1e-7`, so + // they are ORDINARY keys). + let rendered = crate::string::js_number_to_string(value); + if rendered.is_null() { + return false; } + unsafe { string_header_str(rendered as *const crate::string::StringHeader) == Some(name) } } fn typed_array_string_key_kind(name: &str, len: u32) -> TypedArrayStringKeyKind { @@ -307,6 +312,36 @@ fn throw_typed_array_define_error(message: String) -> ! { throw_type_error(message.as_bytes()) } +thread_local! { + /// Typed arrays marked non-extensible by `Object.preventExtensions`. + /// A SIDE TABLE, not the GC-header flag: small typed arrays are plain + /// `alloc`ed without a `GcHeader`, so flag reads/writes at `addr - 8` + /// would touch allocator metadata (observed as random `NO_EXTEND` reads + /// and heap corruption). + static TYPED_ARRAY_NO_EXTEND: RefCell> = + RefCell::new(std::collections::HashSet::new()); +} + +/// Mark a typed array non-extensible (`Object.preventExtensions(ta)`). +pub(crate) fn typed_array_mark_no_extend(owner: usize) { + TYPED_ARRAY_NO_EXTEND.with(|s| { + s.borrow_mut().insert(owner); + }); +} + +/// Has `Object.preventExtensions(ta)` run for this typed array? +pub(crate) fn typed_array_owner_no_extend(owner: usize) -> bool { + TYPED_ARRAY_NO_EXTEND.with(|s| s.borrow().contains(&owner)) +} + +/// Drop the non-extensible mark when a typed array is collected (called from +/// `unregister_typed_array`, mirroring the own-props cleanup). +pub(crate) fn typed_array_clear_no_extend(owner: usize) { + TYPED_ARRAY_NO_EXTEND.with(|s| { + s.borrow_mut().remove(&owner); + }); +} + #[cold] fn throw_type_error(message: &[u8]) -> ! { let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); @@ -353,6 +388,17 @@ pub(crate) unsafe fn typed_array_define_own_property( throw_type_error(b"Invalid typed array index"); } TypedArrayStringKeyKind::Ordinary => { + // OrdinaryDefineOwnProperty step 2: a brand-new key on a + // non-extensible typed array is rejected (`Object.defineProperty` + // throws; the `Reflect` path pre-checks extensibility itself and + // returns false before reaching here). + if !typed_array_has_ordinary_own_prop(owner, key_name) + && typed_array_owner_no_extend(owner) + { + throw_typed_array_define_error(format!( + "Cannot define property {key_name}, object is not extensible" + )); + } let has_get = descriptor_has(desc_ptr, b"get"); let has_set = descriptor_has(desc_ptr, b"set"); let has_accessor = has_get || has_set; @@ -620,6 +666,150 @@ pub(crate) unsafe fn typed_array_has_own_property( } } +/// Full `[[HasProperty]]` for a TypedArray (`key in ta`): a canonical numeric +/// index resolves by bounds only (never the prototype chain), while an +/// ordinary key falls back to OrdinaryHasProperty — own expandos, then the +/// `[[Prototype]]` chain (`%TypedArray%.prototype` methods/accessors, the +/// per-kind prototype, then `Object.prototype`). +pub(crate) unsafe fn typed_array_has_property( + ta: *const TypedArrayHeader, + key: *const crate::string::StringHeader, +) -> bool { + if ta.is_null() || key.is_null() { + return false; + } + let Some(name) = string_header_str(key) else { + return false; + }; + let owner = ta as usize; + match typed_array_string_key_kind(name, typed_array_owner_length(owner)) { + TypedArrayStringKeyKind::InBoundsIndex(_) => true, + TypedArrayStringKeyKind::IntegerIndex => false, + TypedArrayStringKeyKind::Ordinary => { + typed_array_has_ordinary_own_prop(owner, name) + || typed_array_prototype_chain_has(owner, name) + } + } +} + +/// Would an ordinary string key resolve somewhere on a typed array's +/// `[[Prototype]]` chain? Checks the shared `%TypedArray%.prototype` intrinsic +/// object (spec methods + the reflectable accessors), the per-kind prototype +/// object (`Float64Array.prototype` — `constructor` and any user patches), +/// and finally `Object.prototype` (its universal methods + user expandos). +unsafe fn typed_array_prototype_chain_has(owner: usize, name: &str) -> bool { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + // %TypedArray%.prototype intrinsic. + let intrinsic = crate::object::typed_array_intrinsic_proto_ptr(); + if !intrinsic.is_null() { + if crate::object::own_key_present(intrinsic, key) { + return true; + } + if crate::object::get_accessor_descriptor(intrinsic as usize, name).is_some() { + return true; + } + } + // Per-kind prototype object (constructor, user patches). + if name == "constructor" { + return true; + } + if let Some(kind) = typed_array_owner_kind_id(owner) { + let ctor_name = crate::typedarray::name_for_kind(kind); + let ctor = + crate::object::js_get_global_this_builtin_value(ctor_name.as_ptr(), ctor_name.len()); + let raw = crate::value::js_nanbox_get_pointer(ctor) as usize; + if raw >= 0x10000 { + let proto = crate::closure::closure_get_dynamic_prop(raw, "prototype"); + let proto_raw = crate::value::js_nanbox_get_pointer(proto) as usize; + if proto_raw >= 0x10000 { + if crate::object::own_key_present( + proto_raw as *mut crate::object::ObjectHeader, + key, + ) { + return true; + } + if crate::object::get_accessor_descriptor(proto_raw, name).is_some() { + return true; + } + } + } + } + // Object.prototype: universal methods plus user expandos. + if matches!( + name, + "toString" + | "toLocaleString" + | "valueOf" + | "hasOwnProperty" + | "isPrototypeOf" + | "propertyIsEnumerable" + | "__proto__" + ) { + return true; + } + let obj_proto = crate::object::builtin_prototype_value("Object"); + let obj_proto_raw = crate::value::js_nanbox_get_pointer(obj_proto) as usize; + if obj_proto_raw >= 0x10000 { + if crate::object::own_key_present(obj_proto_raw as *mut crate::object::ObjectHeader, key) { + return true; + } + if crate::object::get_accessor_descriptor(obj_proto_raw, name).is_some() { + return true; + } + } + false +} + +/// The element kind for a TypedArray owner address (`None` for the +/// `BufferHeader`-backed `Uint8Array` representation). +fn typed_array_owner_kind_id(owner: usize) -> Option { + lookup_typed_array_kind(owner) +} + +/// Classify a string key against a typed array's CanonicalNumericIndexString +/// rule: `Some(true)` = valid in-bounds integer index, `Some(false)` = +/// canonical numeric index that is NOT a valid index (out of bounds, `-1`, +/// `1.5`, `-0`, …), `None` = ordinary key. Used by the exotic `[[Set]]` +/// interception (a canonical index never consults the prototype chain). +pub(crate) fn typed_array_canonical_index_validity(owner: usize, name: &str) -> Option { + let len = unsafe { typed_array_owner_length(owner) }; + match typed_array_string_key_kind(name, len) { + TypedArrayStringKeyKind::InBoundsIndex(_) => Some(true), + TypedArrayStringKeyKind::IntegerIndex => Some(false), + TypedArrayStringKeyKind::Ordinary => None, + } +} + +/// `OrdinaryToPrimitive(O, number)` own-expando probe for a typed array used +/// as a *coercion source*: a patched own `valueOf`/`toString` (stored in the +/// typed-array own-props side table, invisible to the generic object helpers) +/// runs with `this` = the view, propagating abrupt completions. Returns +/// `Some(primitive)` when a patched method produced a non-object; `None` when +/// no own patch applies (caller falls back to its default coercion). +pub(crate) unsafe fn typed_array_own_to_primitive_number(owner: usize, value: f64) -> Option { + for name in ["valueOf", "toString"] { + let Some(m) = typed_array_get_property_value_by_name(owner, name) else { + continue; + }; + let mbits = m.to_bits(); + if (mbits >> 48) != 0x7FFD + || !crate::closure::is_closure_ptr((mbits & crate::value::POINTER_MASK) as usize) + { + continue; + } + let bound = crate::closure::clone_closure_rebind_this(mbits, value); + let r = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); + let rb = r.to_bits(); + let is_object = (rb >> 48) == 0x7FFD + && crate::symbol::js_is_symbol(r) == 0 + && (rb & crate::value::POINTER_MASK) >= 0x10000; + if !is_object { + return Some(r); + } + } + None +} + pub(crate) unsafe fn typed_array_property_is_enumerable( ta: *const TypedArrayHeader, key: *const crate::string::StringHeader, diff --git a/crates/perry-runtime/src/typedarray_view.rs b/crates/perry-runtime/src/typedarray_view.rs index d86f2931d4..1ef3f78136 100644 --- a/crates/perry-runtime/src/typedarray_view.rs +++ b/crates/perry-runtime/src/typedarray_view.rs @@ -25,14 +25,21 @@ fn typed_array_view_to_index(value: f64) -> i64 { if jv.is_undefined() { return 0; } - let n = jv.to_number(); + // Real `ToNumber`: runs `valueOf`/`Symbol.toPrimitive` on objects + // (observable, may throw) and throws TypeError on a Symbol or BigInt + // argument — `jv.to_number()` did none of that, so an object byteOffset + // silently became 0 and Symbol offsets/lengths never threw. + let n = crate::typedarray::jsvalue_to_f64(value); if n.is_nan() { return 0; } - if n < 0.0 || n > 9_007_199_254_740_991.0 { + // `ToIntegerOrInfinity` truncates BEFORE the range check, so a value in + // (-1, 0) is 0, not a RangeError. + let integer = n.trunc(); + if integer < 0.0 || integer > 9_007_199_254_740_991.0 { throw_range_error(b"Invalid typed array length"); } - n.trunc() as i64 + integer as i64 } /// `new TA(buffer, byteOffset, length?)` for the non-`Uint8Array` typed-array