Skip to content

Commit 4e464ce

Browse files
committed
Fix Array, Object, DataView, TypedArray, and Map built-in conformance
- Rewrite Array.prototype.flat/flatMap with spec-compliant FlattenIntoArray - Add ToIntegerOrInfinity, LengthOfArrayLike, ToLength helper functions - Add HasProperty support for Proxy and TypedArray in array flattening - Add findLast/findLastIndex to Array Symbol.unscopables list - Fix findLast/findLastIndex to use LengthOfArrayLike (clamp to 2^53-1) - Add Array.prototype.at and flatMap to function .length tables - Rewrite Object.fromEntries with IteratorClose, ToPropertyKey, symbol support - Rewrite Object.groupBy to use iterator protocol and support string iterables - Add public IsCallable helper (is_callable_for_from_entries) - Fix DataView set methods to use modular integer conversion (not saturating cast) - Fix TypedArray.prototype.at to call ToNumber with valueOf support - Implement Map.groupBy static method with SameValueZero key matching
1 parent f4862ff commit 4e464ce

File tree

5 files changed

+529
-195
lines changed

5 files changed

+529
-195
lines changed

src/core/eval.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12718,7 +12718,9 @@ pub fn evaluate_call_dispatch<'gc>(
1271812718
let this_v = this_val.unwrap_or(&Value::Undefined);
1271912719
Ok(this_v.clone())
1272012720
} else if name.starts_with("Map.") {
12721-
if let Some(method) = name.strip_prefix("Map.prototype.") {
12721+
if name == "Map.groupBy" {
12722+
Ok(crate::js_map::handle_map_group_by(mc, eval_args, env)?)
12723+
} else if let Some(method) = name.strip_prefix("Map.prototype.") {
1272212724
let this_v = this_val.unwrap_or(&Value::Undefined);
1272312725
if let Value::Object(obj) = this_v {
1272412726
if let Some(map_val) = slot_get_chained(obj, &InternalSlot::Map) {
@@ -16412,6 +16414,8 @@ fn evaluate_expr_property<'gc>(
1641216414
}
1641316415
let len = match func_name.as_str() {
1641416416
"Array.prototype.push"
16417+
| "Array.prototype.at"
16418+
| "Array.prototype.flatMap"
1641516419
| "Array.prototype.indexOf"
1641616420
| "Array.prototype.lastIndexOf"
1641716421
| "Array.prototype.concat"
@@ -16549,6 +16553,7 @@ fn evaluate_expr_property<'gc>(
1654916553
| "Object.create"
1655016554
| "Object.defineProperties"
1655116555
| "Object.getOwnPropertyDescriptor"
16556+
| "Map.groupBy"
1655216557
| "Object.groupBy"
1655316558
| "Object.hasOwn"
1655416559
| "Object.is"
@@ -17862,6 +17867,8 @@ fn evaluate_expr_index<'gc>(
1786217867
}
1786317868
let len = match func_name.as_str() {
1786417869
"Array.prototype.push"
17870+
| "Array.prototype.at"
17871+
| "Array.prototype.flatMap"
1786517872
| "Array.prototype.indexOf"
1786617873
| "Array.prototype.lastIndexOf"
1786717874
| "Array.prototype.concat"
@@ -18001,6 +18008,7 @@ fn evaluate_expr_index<'gc>(
1800118008
| "Object.create"
1800218009
| "Object.defineProperties"
1800318010
| "Object.getOwnPropertyDescriptor"
18011+
| "Map.groupBy"
1800418012
| "Object.groupBy"
1800518013
| "Object.hasOwn"
1800618014
| "Object.is"

src/js_array.rs

Lines changed: 193 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,8 @@ pub fn initialize_array<'gc>(mc: &MutationContext<'gc>, env: &JSObjectDataPtr<'g
302302
"fill",
303303
"find",
304304
"findIndex",
305+
"findLast",
306+
"findLastIndex",
305307
"flat",
306308
"flatMap",
307309
"includes",
@@ -4351,64 +4353,67 @@ pub(crate) fn handle_array_instance_method<'gc>(
43514353
Ok(Value::String(utf8_to_utf16(&result)))
43524354
}
43534355
"flat" => {
4354-
let depth = if !args.is_empty() {
4355-
match args[0].clone() {
4356-
Value::Number(n) => n as usize,
4357-
_ => 1,
4358-
}
4356+
// §23.1.3.12 Array.prototype.flat ( [ depth ] )
4357+
// 1. Let O be ? ToObject(this value).
4358+
// 2. Let sourceLen be ? LengthOfArrayLike(O).
4359+
let source_len = length_of_array_like(mc, env, object)?;
4360+
4361+
// 3. Let depthNum be 1 (default).
4362+
// 4. If depth is not undefined, let depthNum be ? ToIntegerOrInfinity(depth).
4363+
let depth_num: f64 = if args.is_empty() || matches!(args[0], Value::Undefined) {
4364+
1.0
43594365
} else {
4360-
1
4366+
to_integer_or_infinity_local(mc, env, &args[0])?
43614367
};
43624368

4363-
let mut result = Vec::new();
4364-
flatten_array(mc, object, &mut result, depth)?;
4365-
4369+
// 5. Let A be ? ArraySpeciesCreate(O, 0).
43664370
let new_array = array_species_create_impl(mc, env, object, 0.0)?;
4367-
for (i, val) in result.iter().enumerate() {
4368-
create_data_property_or_throw(mc, &new_array, i, val)?;
4371+
4372+
// 6. Perform ? FlattenIntoArray(A, O, sourceLen, 0, depthNum).
4373+
flatten_into_array_spec(mc, env, &new_array, object, source_len, 0, depth_num, None, None)?;
4374+
4375+
// 7. Return A.
4376+
// For true arrays, update length; for species objects, leave it alone.
4377+
if is_array(mc, &new_array) {
4378+
let final_len = object_get_length(&new_array).unwrap_or(0);
4379+
set_array_length(mc, &new_array, final_len)?;
43694380
}
4370-
set_array_length(mc, &new_array, result.len())?;
43714381
Ok(Value::Object(new_array))
43724382
}
43734383
"flatMap" => {
4374-
if args.is_empty() {
4375-
return Err(raise_eval_error!("Array.flatMap expects at least one argument").into());
4376-
}
4377-
4378-
let callback_val = args[0].clone();
4379-
let current_len = get_array_length(mc, object).unwrap_or(0);
4384+
// §23.1.3.11 Array.prototype.flatMap ( mapperFunction [ , thisArg ] )
4385+
// 1. Let O be ? ToObject(this value).
4386+
// 2. Let sourceLen be ? LengthOfArrayLike(O).
4387+
let source_len = length_of_array_like(mc, env, object)?;
43804388

4381-
let mut result = Vec::new();
4382-
for i in 0..current_len {
4383-
if let Some(val) = object_get_key_value(object, i) {
4384-
// Support inline closures wrapped as objects with internal closure.
4385-
let actual_func = if let Value::Object(obj) = &callback_val {
4386-
if let Some(prop) = obj.borrow().get_closure() {
4387-
prop.borrow().clone()
4388-
} else {
4389-
callback_val.clone()
4390-
}
4391-
} else {
4392-
callback_val.clone()
4393-
};
4394-
4395-
let args = vec![val.borrow().clone(), Value::Number(i as f64), Value::Object(*object)];
4396-
4397-
let mapped_val = match &actual_func {
4398-
Value::Closure(cl) => crate::core::call_closure(mc, cl, None, &args, env, None)?,
4399-
Value::Function(name) => crate::js_function::handle_global_function(mc, name, &args, env)?,
4400-
_ => return Err(raise_eval_error!("Array.flatMap expects a function").into()),
4401-
};
4402-
4403-
flatten_single_value(mc, &mapped_val, &mut result, 1)?;
4404-
}
4389+
// 3. If IsCallable(mapperFunction) is false, throw a TypeError exception.
4390+
let callback_val = args.first().cloned().unwrap_or(Value::Undefined);
4391+
if !is_callable_val(&callback_val) {
4392+
return Err(raise_type_error!("flatMap mapper is not a function").into());
44054393
}
44064394

4395+
// 4. Let A be ? ArraySpeciesCreate(O, 0).
44074396
let new_array = array_species_create_impl(mc, env, object, 0.0)?;
4408-
for (i, val) in result.iter().enumerate() {
4409-
create_data_property_or_throw(mc, &new_array, i, val)?;
4397+
4398+
// 5. Perform ? FlattenIntoArray(A, O, sourceLen, 0, 1, mapperFunction, thisArg).
4399+
let this_arg = args.get(1).cloned().unwrap_or(Value::Undefined);
4400+
flatten_into_array_spec(
4401+
mc,
4402+
env,
4403+
&new_array,
4404+
object,
4405+
source_len,
4406+
0,
4407+
1.0,
4408+
Some(&callback_val),
4409+
Some(&this_arg),
4410+
)?;
4411+
4412+
// 6. Return A.
4413+
if is_array(mc, &new_array) {
4414+
let final_len = object_get_length(&new_array).unwrap_or(0);
4415+
set_array_length(mc, &new_array, final_len)?;
44104416
}
4411-
set_array_length(mc, &new_array, result.len())?;
44124417
Ok(Value::Object(new_array))
44134418
}
44144419
"copyWithin" => {
@@ -4630,16 +4635,8 @@ pub(crate) fn handle_array_instance_method<'gc>(
46304635
let callback_val = args.first().cloned().unwrap_or(Value::Undefined);
46314636
let this_arg = args.get(1).cloned().unwrap_or(Value::Undefined);
46324637

4633-
let len_val = crate::core::get_property_with_accessors(mc, env, object, "length")?;
4634-
let len_prim = crate::core::to_primitive(mc, &len_val, "number", env)?;
4635-
let len_num = crate::core::to_number(&len_prim)?;
4636-
let current_len = if len_num.is_nan() || len_num <= 0.0 {
4637-
0usize
4638-
} else if !len_num.is_finite() {
4639-
usize::MAX
4640-
} else {
4641-
len_num.floor() as usize
4642-
};
4638+
// 2. Let len be ? LengthOfArrayLike(O).
4639+
let current_len = length_of_array_like(mc, env, object)?;
46434640

46444641
let callback_callable = match &callback_val {
46454642
Value::Closure(_)
@@ -4692,16 +4689,8 @@ pub(crate) fn handle_array_instance_method<'gc>(
46924689
let callback_val = args.first().cloned().unwrap_or(Value::Undefined);
46934690
let this_arg = args.get(1).cloned().unwrap_or(Value::Undefined);
46944691

4695-
let len_val = crate::core::get_property_with_accessors(mc, env, object, "length")?;
4696-
let len_prim = crate::core::to_primitive(mc, &len_val, "number", env)?;
4697-
let len_num = crate::core::to_number(&len_prim)?;
4698-
let current_len = if len_num.is_nan() || len_num <= 0.0 {
4699-
0usize
4700-
} else if !len_num.is_finite() {
4701-
usize::MAX
4702-
} else {
4703-
len_num.floor() as usize
4704-
};
4692+
// 2. Let len be ? LengthOfArrayLike(O).
4693+
let current_len = length_of_array_like(mc, env, object)?;
47054694

47064695
let callback_callable = match &callback_val {
47074696
Value::Closure(_)
@@ -4748,48 +4737,156 @@ pub(crate) fn handle_array_instance_method<'gc>(
47484737
}
47494738

47504739
// Helper functions for array flattening
4751-
fn flatten_array<'gc>(
4740+
/// ToIntegerOrInfinity (local helper for array methods)
4741+
fn to_integer_or_infinity_local<'gc>(
47524742
mc: &MutationContext<'gc>,
4753-
object: &JSObjectDataPtr<'gc>,
4754-
result: &mut Vec<Value<'gc>>,
4755-
depth: usize,
4756-
) -> Result<(), JSError> {
4757-
let current_len = get_array_length(mc, object).unwrap_or(0);
4758-
4759-
for i in 0..current_len {
4760-
if let Some(val) = object_get_key_value(object, i) {
4761-
flatten_single_value(mc, &val.borrow(), result, depth)?;
4762-
}
4743+
env: &JSObjectDataPtr<'gc>,
4744+
val: &Value<'gc>,
4745+
) -> Result<f64, EvalError<'gc>> {
4746+
let n = crate::core::to_number_with_env(mc, env, val)?;
4747+
if n.is_nan() || n == 0.0 {
4748+
Ok(0.0)
4749+
} else if !n.is_finite() {
4750+
Ok(n)
4751+
} else {
4752+
Ok(n.trunc())
47634753
}
4764-
Ok(())
47654754
}
47664755

4767-
fn flatten_single_value<'gc>(
4756+
/// LengthOfArrayLike(obj) — spec 7.3.2
4757+
/// Returns ℝ(ToLength(Get(obj, "length"))).
4758+
fn length_of_array_like<'gc>(
47684759
mc: &MutationContext<'gc>,
4769-
value: &Value<'gc>,
4770-
result: &mut Vec<Value<'gc>>,
4771-
depth: usize,
4772-
) -> Result<(), JSError> {
4773-
if depth == 0 {
4774-
result.push(value.clone());
4775-
return Ok(());
4760+
env: &JSObjectDataPtr<'gc>,
4761+
obj: &JSObjectDataPtr<'gc>,
4762+
) -> Result<usize, EvalError<'gc>> {
4763+
let len_val = crate::core::get_property_with_accessors(mc, env, obj, "length")?;
4764+
to_length(mc, env, &len_val)
4765+
}
4766+
4767+
/// ToLength(argument) — spec 7.1.20
4768+
fn to_length<'gc>(mc: &MutationContext<'gc>, env: &JSObjectDataPtr<'gc>, val: &Value<'gc>) -> Result<usize, EvalError<'gc>> {
4769+
let len = to_integer_or_infinity_local(mc, env, val)?;
4770+
if len <= 0.0 {
4771+
Ok(0)
4772+
} else {
4773+
Ok((len.min(9007199254740991.0)) as usize)
47764774
}
4775+
}
47774776

4778-
match value {
4777+
/// IsCallable check (mirrors spec § 7.2.3)
4778+
fn is_callable_val<'gc>(val: &Value<'gc>) -> bool {
4779+
match val {
4780+
Value::Function(_)
4781+
| Value::Closure(_)
4782+
| Value::AsyncClosure(_)
4783+
| Value::GeneratorFunction(..)
4784+
| Value::AsyncGeneratorFunction(..) => true,
47794785
Value::Object(obj) => {
4780-
// Check if it's an array-like object
4781-
let is_arr = { is_array(mc, obj) };
4782-
if is_arr {
4783-
flatten_array(mc, obj, result, depth - 1)?;
4784-
} else {
4785-
result.push(Value::Object(*obj));
4786-
}
4786+
obj.borrow().get_closure().is_some()
4787+
|| obj.borrow().class_def.is_some()
4788+
|| crate::core::slot_get(obj, &InternalSlot::NativeCtor).is_some()
4789+
|| crate::core::slot_get(obj, &InternalSlot::BoundTarget).is_some()
4790+
|| crate::core::slot_get(obj, &InternalSlot::Callable).is_some()
4791+
}
4792+
_ => false,
4793+
}
4794+
}
4795+
4796+
/// HasProperty through proxy / TypedArray if needed
4797+
fn has_property_spec<'gc>(mc: &MutationContext<'gc>, obj: &JSObjectDataPtr<'gc>, key: &str) -> Result<bool, EvalError<'gc>> {
4798+
if let Some(proxy_cell) = crate::core::slot_get(obj, &InternalSlot::Proxy)
4799+
&& let Value::Proxy(proxy) = &*proxy_cell.borrow()
4800+
{
4801+
Ok(crate::js_proxy::proxy_has_property(mc, proxy, key.to_string())?)
4802+
} else if let Some(ta_cell) = crate::core::slot_get(obj, &InternalSlot::TypedArray) {
4803+
// TypedArray [[HasProperty]]: check if key is a valid integer index
4804+
if let Value::TypedArray(ta) = &*ta_cell.borrow()
4805+
&& let Some(idx_f) = crate::js_typedarray::canonical_numeric_index_string(key)
4806+
{
4807+
return Ok(crate::js_typedarray::is_valid_integer_index(ta, idx_f));
4808+
}
4809+
// Non-numeric key: fall through to own properties
4810+
Ok(object_get_key_value(obj, key).is_some())
4811+
} else {
4812+
Ok(object_get_key_value(obj, key).is_some())
4813+
}
4814+
}
4815+
4816+
/// FlattenIntoArray — spec 23.1.3.11.1
4817+
/// target: the result array to populate
4818+
/// source: the current array being flattened
4819+
/// source_len: length of source
4820+
/// start: target start index
4821+
/// depth: remaining depth
4822+
/// mapper_function: optional mapper (for flatMap only)
4823+
/// this_arg: optional thisArg for mapper
4824+
/// Returns: the next target index after insertion
4825+
#[allow(clippy::too_many_arguments)]
4826+
fn flatten_into_array_spec<'gc>(
4827+
mc: &MutationContext<'gc>,
4828+
env: &JSObjectDataPtr<'gc>,
4829+
target: &JSObjectDataPtr<'gc>,
4830+
source: &JSObjectDataPtr<'gc>,
4831+
source_len: usize,
4832+
start: usize,
4833+
depth: f64,
4834+
mapper_function: Option<&Value<'gc>>,
4835+
this_arg: Option<&Value<'gc>>,
4836+
) -> Result<usize, EvalError<'gc>> {
4837+
let mut target_index = start;
4838+
4839+
for source_index in 0..source_len {
4840+
let p = source_index.to_string();
4841+
4842+
// b. Let exists be ? HasProperty(source, P).
4843+
if !has_property_spec(mc, source, &p)? {
4844+
continue;
47874845
}
4788-
_ => {
4789-
result.push(value.clone());
4846+
4847+
// c.i. Let element be ? Get(source, P).
4848+
let mut element = crate::core::get_property_with_accessors(mc, env, source, &*p)?;
4849+
4850+
// c.ii. If mapperFunction is present, let element be ? Call(mapperFunction, thisArg, «element, sourceIndex, source»).
4851+
if let Some(mapper) = mapper_function {
4852+
let default_this = Value::Undefined;
4853+
let t = this_arg.unwrap_or(&default_this);
4854+
let call_args = vec![element, Value::Number(source_index as f64), Value::Object(*source)];
4855+
element = crate::core::evaluate_call_dispatch(mc, env, mapper, Some(t), &call_args)?;
4856+
}
4857+
4858+
// c.iii. Let shouldFlatten be false.
4859+
// c.iv. If depth > 0, let shouldFlatten be ? IsArray(element).
4860+
let should_flatten = if depth > 0.0 {
4861+
match &element {
4862+
Value::Object(obj) => is_array_spec(mc, obj)?,
4863+
_ => false,
4864+
}
4865+
} else {
4866+
false
4867+
};
4868+
4869+
if should_flatten {
4870+
// c.v. If shouldFlatten is true:
4871+
// 1. If depth is +∞, let newDepth be +∞. Otherwise, let newDepth be depth - 1.
4872+
let new_depth = if depth == f64::INFINITY { f64::INFINITY } else { depth - 1.0 };
4873+
if let Value::Object(inner_obj) = &element {
4874+
let inner_len = length_of_array_like(mc, env, inner_obj)?;
4875+
target_index = flatten_into_array_spec(mc, env, target, inner_obj, inner_len, target_index, new_depth, None, None)?;
4876+
}
4877+
} else {
4878+
// c.vi. Else:
4879+
// 1. If targetIndex ≥ 2^53 - 1, throw a TypeError.
4880+
if target_index >= 9007199254740991 {
4881+
return Err(raise_type_error!("FlattenIntoArray: target index exceeds safe integer limit").into());
4882+
}
4883+
// 2. Perform ? CreateDataPropertyOrThrow(target, targetIndex, element).
4884+
create_data_property_or_throw(mc, target, target_index, &element)?;
4885+
target_index += 1;
47904886
}
47914887
}
4792-
Ok(())
4888+
4889+
Ok(target_index)
47934890
}
47944891

47954892
/// Check if an object is an Array

0 commit comments

Comments
 (0)