From 49dd95a2495bf55359d714441232a1e2ba63d77a Mon Sep 17 00:00:00 2001 From: yush-1018 Date: Sun, 8 Mar 2026 13:06:37 +0530 Subject: [PATCH 01/12] feat: implement Iterator.zip and Iterator.zipKeyed (#4564) --- core/engine/src/builtins/iterable/mod.rs | 289 +++++++++++- .../src/builtins/iterable/zip_iterator.rs | 433 ++++++++++++++++++ 2 files changed, 721 insertions(+), 1 deletion(-) create mode 100644 core/engine/src/builtins/iterable/zip_iterator.rs diff --git a/core/engine/src/builtins/iterable/mod.rs b/core/engine/src/builtins/iterable/mod.rs index bbeda81acba..c4def5ec797 100644 --- a/core/engine/src/builtins/iterable/mod.rs +++ b/core/engine/src/builtins/iterable/mod.rs @@ -1,7 +1,7 @@ //! Boa's implementation of ECMAScript's `IteratorRecord` and iterator prototype objects. use crate::{ - Context, JsResult, JsValue, + Context, JsArgs, JsResult, JsValue, builtins::{BuiltInBuilder, IntrinsicObject}, context::intrinsics::Intrinsics, error::JsNativeError, @@ -22,6 +22,9 @@ mod tests; pub(crate) use async_from_sync_iterator::AsyncFromSyncIterator; +mod zip_iterator; +pub(crate) use zip_iterator::{ZipIterator, ZipMode, ZipResultKind}; + /// `IfAbruptCloseIterator ( value, iteratorRecord )` /// /// `IfAbruptCloseIterator` is a shorthand for a sequence of algorithm steps that use an `Iterator` @@ -196,6 +199,8 @@ impl IntrinsicObject for Iterator { fn init(realm: &Realm) { BuiltInBuilder::with_intrinsic::(realm) .static_method(|v, _, _| Ok(v.clone()), JsSymbol::iterator(), 0) + .static_method(Self::zip, js_string!("zip"), 1) + .static_method(Self::zip_keyed, js_string!("zipKeyed"), 1) .build(); } @@ -204,6 +209,288 @@ impl IntrinsicObject for Iterator { } } +impl Iterator { + /// `Iterator.zip ( iterables [ , options ] )` + /// + /// More information: + /// - [TC39 proposal][spec] + /// + /// [spec]: https://tc39.es/proposal-joint-iteration/#sec-iterator.zip + fn zip(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let iterables = args.get_or_undefined(0); + let options = args.get_or_undefined(1); + + // 1. If iterables is not an Object, throw a TypeError exception. + let iterables_obj = iterables.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("Iterator.zip requires an iterable object") + })?; + + // 2-5. Parse mode from options (default "shortest"). + let mode = Self::parse_zip_mode(options, context)?; + + // 6-7. Parse padding option (only for "longest" mode). + let padding_option = if mode == ZipMode::Longest { + let opts_obj = options.as_object(); + if let Some(opts) = opts_obj { + let p = opts.get(js_string!("padding"), context)?; + if !p.is_undefined() { + if !p.is_object() { + return Err(JsNativeError::typ() + .with_message("padding must be an object") + .into()); + } + Some(p) + } else { + None + } + } else { + None + } + } else { + None + }; + + // 8-11. Collect iterator records from iterables. + let iterables_val: JsValue = iterables_obj.clone().into(); + let mut input_iter = iterables_val.get_iterator(IteratorHint::Sync, context)?; + let mut iters: Vec = Vec::new(); + + loop { + let next = input_iter.step_value(context); + match next { + Err(err) => { + // IfAbruptCloseIterators(next, iters) + for iter in &iters { + let _ = iter.close(Ok(JsValue::undefined()), context); + } + return Err(err); + } + Ok(None) => break, // done + Ok(Some(value)) => { + // GetIteratorFlattenable(next, reject-primitives) + if !value.is_object() { + // Close all collected iterators and the input iterator. + for iter in &iters { + let _ = iter.close(Ok(JsValue::undefined()), context); + } + let _ = input_iter.close(Ok(JsValue::undefined()), context); + return Err(JsNativeError::typ() + .with_message("iterator value is not an object") + .into()); + } + let iter_result = value.get_iterator(IteratorHint::Sync, context); + match iter_result { + Err(err) => { + for iter in &iters { + let _ = iter.close(Ok(JsValue::undefined()), context); + } + let _ = input_iter.close(Ok(JsValue::undefined()), context); + return Err(err); + } + Ok(iter) => iters.push(iter), + } + } + } + } + + let iter_count = iters.len(); + + // 12-16. Build padding list. + let padding = Self::build_padding(padding_option, iter_count, &iters, context)?; + + // Return IteratorZip(iters, mode, padding, finishResults). + Ok(ZipIterator::create_zip_iterator( + iters, + mode, + padding, + ZipResultKind::Array, + context, + )) + } + + /// `Iterator.zipKeyed ( iterables [ , options ] )` + /// + /// More information: + /// - [TC39 proposal][spec] + /// + /// [spec]: https://tc39.es/proposal-joint-iteration/#sec-iterator.zipkeyed + fn zip_keyed(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let iterables = args.get_or_undefined(0); + let options = args.get_or_undefined(1); + + // 1. If iterables is not an Object, throw a TypeError exception. + let iterables_obj = iterables.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("Iterator.zipKeyed requires an object") + })?; + + // 2-5. Parse mode from options. + let mode = Self::parse_zip_mode(options, context)?; + + // 6-7. Parse padding option. + let padding_option = if mode == ZipMode::Longest { + let opts_obj = options.as_object(); + if let Some(opts) = opts_obj { + let p = opts.get(js_string!("padding"), context)?; + if !p.is_undefined() { + if !p.is_object() { + return Err(JsNativeError::typ() + .with_message("padding must be an object") + .into()); + } + Some(p) + } else { + None + } + } else { + None + } + } else { + None + }; + + // 8-10. Get own enumerable string-keyed properties and their iterator values. + let mut iters: Vec = Vec::new(); + let mut keys: Vec = Vec::new(); + + let all_keys = iterables_obj.own_property_keys(context)?; + for key in all_keys { + let key_val: JsValue = key.clone().into(); + let value = iterables_obj.get(key.clone(), context)?; + if !value.is_undefined() { + keys.push(key_val); + if !value.is_object() { + for iter in &iters { + let _ = iter.close(Ok(JsValue::undefined()), context); + } + return Err(JsNativeError::typ() + .with_message("iterator value is not an object") + .into()); + } + let iter = value.get_iterator(IteratorHint::Sync, context); + match iter { + Err(err) => { + for it in &iters { + let _ = it.close(Ok(JsValue::undefined()), context); + } + return Err(err); + } + Ok(iter) => iters.push(iter), + } + } + } + + let iter_count = iters.len(); + + // Build padding for zipKeyed. + let padding = if mode == ZipMode::Longest { + match padding_option { + None => vec![JsValue::undefined(); iter_count], + Some(pad_obj) => { + let pad = pad_obj.as_object().unwrap(); + let mut padding = Vec::with_capacity(iter_count); + for key in &keys { + let prop_key = key.to_string(context) + .unwrap_or_default(); + let val = pad.get(prop_key, context)?; + padding.push(val); + } + padding + } + } + } else { + Vec::new() + }; + + Ok(ZipIterator::create_zip_iterator( + iters, + mode, + padding, + ZipResultKind::Keyed(keys), + context, + )) + } + + /// Parses the `mode` option from the options object. + fn parse_zip_mode(options: &JsValue, context: &mut Context) -> JsResult { + if options.is_undefined() || options.is_null() { + return Ok(ZipMode::Shortest); + } + let opts = options.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("options must be an object") + })?; + let mode_val = opts.get(js_string!("mode"), context)?; + if mode_val.is_undefined() { + return Ok(ZipMode::Shortest); + } + let mode_str = mode_val.to_string(context)?; + match mode_str.to_std_string_escaped().as_str() { + "shortest" => Ok(ZipMode::Shortest), + "longest" => Ok(ZipMode::Longest), + "strict" => Ok(ZipMode::Strict), + _ => Err(JsNativeError::typ() + .with_message("mode must be \"shortest\", \"longest\", or \"strict\"") + .into()), + } + } + + /// Builds the padding list for "longest" mode. + fn build_padding( + padding_option: Option, + iter_count: usize, + iters: &[IteratorRecord], + context: &mut Context, + ) -> JsResult> { + match padding_option { + None => Ok(vec![JsValue::undefined(); iter_count]), + Some(pad_val) => { + let mut padding_iter = pad_val.get_iterator(IteratorHint::Sync, context) + .map_err(|err| { + for iter in iters { + let _ = iter.close(Ok(JsValue::undefined()), context); + } + err + })?; + let mut padding = Vec::new(); + let mut using_iterator = true; + + for _ in 0..iter_count { + if using_iterator { + match padding_iter.step_value(context) { + Err(err) => { + for iter in iters { + let _ = iter.close(Ok(JsValue::undefined()), context); + } + return Err(err); + } + Ok(None) => { + using_iterator = false; + padding.push(JsValue::undefined()); + } + Ok(Some(val)) => { + padding.push(val); + } + } + } else { + padding.push(JsValue::undefined()); + } + } + + if using_iterator { + let close_result = padding_iter.close(Ok(JsValue::undefined()), context); + if let Err(err) = close_result { + for iter in iters { + let _ = iter.close(Ok(JsValue::undefined()), context); + } + return Err(err); + } + } + + Ok(padding) + } + } + } +} + /// `%AsyncIteratorPrototype%` object /// /// More information: diff --git a/core/engine/src/builtins/iterable/zip_iterator.rs b/core/engine/src/builtins/iterable/zip_iterator.rs new file mode 100644 index 00000000000..8f6eb8644e5 --- /dev/null +++ b/core/engine/src/builtins/iterable/zip_iterator.rs @@ -0,0 +1,433 @@ +//! This module implements the `ZipIterator` object backing `Iterator.zip` and `Iterator.zipKeyed`. +//! +//! More information: +//! - [TC39 proposal][proposal] +//! +//! [proposal]: https://tc39.es/proposal-joint-iteration/ + +use crate::{ + Context, JsData, JsResult, JsValue, + builtins::{ + Array, BuiltInBuilder, IntrinsicObject, + iterable::{IteratorRecord, create_iter_result_object}, + }, + context::intrinsics::Intrinsics, + error::JsNativeError, + js_string, + object::JsObject, + property::Attribute, + realm::Realm, + symbol::JsSymbol, +}; +use boa_gc::{Finalize, Trace}; +use crate::property::PropertyKey; + +/// The mode for zip iteration. +#[derive(Debug, Clone, PartialEq, Eq, Trace, Finalize)] +pub(crate) enum ZipMode { + /// Stops when the shortest iterator is done. + Shortest, + /// Continues until the longest iterator is done, padding with `undefined` or user values. + Longest, + /// All iterators must have the same length, otherwise throws a `TypeError`. + Strict, +} + +/// The kind of result to produce from the zip iterator. +#[derive(Debug, Clone, Trace, Finalize)] +pub(crate) enum ZipResultKind { + /// Produces arrays (for `Iterator.zip`). + Array, + /// Produces objects with the given keys (for `Iterator.zipKeyed`). + Keyed(Vec), +} + +/// The `ZipIterator` object represents a joint iteration over multiple iterators. +/// +/// It implements the iterator protocol and is returned by `Iterator.zip()` and `Iterator.zipKeyed()`. +/// +/// More information: +/// - [TC39 proposal][proposal] +/// +/// [proposal]: https://tc39.es/proposal-joint-iteration/ +#[derive(Debug, Finalize, Trace, JsData)] +pub(crate) struct ZipIterator { + /// The list of underlying iterator records. An entry is set to `None` when exhausted + /// (only relevant in "longest" mode). + iters: Vec>, + + /// The total number of iterators (does not change when iterators are exhausted). + iter_count: usize, + + /// The list of iterators that are still open (indices into `iters`). + /// When this becomes empty in "longest" mode, iteration is done. + open_iters: Vec, + + /// The iteration mode. + #[unsafe_ignore_trace] + mode: ZipMode, + + /// Padding values for "longest" mode. + padding: Vec, + + /// What kind of result object to produce. + result_kind: ZipResultKind, + + /// Whether the iterator has been completed. + done: bool, +} + +impl ZipIterator { + /// Creates a new `ZipIterator`. + pub(crate) fn new( + iters: Vec, + mode: ZipMode, + padding: Vec, + result_kind: ZipResultKind, + ) -> Self { + let iter_count = iters.len(); + let open_iters: Vec = (0..iter_count).collect(); + let iters = iters.into_iter().map(Some).collect(); + Self { + iters, + iter_count, + open_iters, + mode, + padding, + result_kind, + done: false, + } + } + + /// Creates a `ZipIterator` JS object and wraps it as a `JsValue`. + pub(crate) fn create_zip_iterator( + iters: Vec, + mode: ZipMode, + padding: Vec, + result_kind: ZipResultKind, + context: &mut Context, + ) -> JsValue { + let zip_iter = Self::new(iters, mode, padding, result_kind); + let obj = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + context + .intrinsics() + .objects() + .iterator_prototypes() + .iterator(), + zip_iter, + ); + obj.into() + } + + /// Closes all open iterators with the given completion. + fn close_all( + iters: &mut Vec>, + open_iters: &[usize], + completion: JsResult, + context: &mut Context, + ) -> JsResult { + let mut result = completion; + for &idx in open_iters { + if let Some(iter) = iters[idx].take() { + let close_result = iter.close(Ok(JsValue::undefined()), context); + if result.is_ok() && close_result.is_err() { + result = close_result; + } + } + } + result + } + + /// Builds the result value from a list of values based on the `ZipResultKind`. + fn finish_results( + results: &[JsValue], + result_kind: &ZipResultKind, + context: &mut Context, + ) -> JsValue { + match result_kind { + ZipResultKind::Array => { + // CreateArrayFromList(results) + Array::create_array_from_list(results.to_vec(), context).into() + } + ZipResultKind::Keyed(keys) => { + // Create a null-prototype object with keys mapped to results. + let obj = JsObject::with_null_proto(); + for (i, key) in keys.iter().enumerate() { + if let Some(val) = results.get(i) { + let prop_key: PropertyKey = key.to_string(context) + .unwrap_or_default() + .into(); + obj.set(prop_key, val.clone(), false, context) + .expect("setting property on new object should not fail"); + } + } + obj.into() + } + } + } + + /// `%ZipIteratorPrototype%.next()` + /// + /// Implements the IteratorZip abstract operation from the TC39 Joint Iteration proposal. + /// + /// More information: + /// - [TC39 proposal][proposal] + /// + /// [proposal]: https://tc39.es/proposal-joint-iteration/#sec-iteratorzip + pub(crate) fn next(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + let obj = this.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("`this` is not a ZipIterator") + })?; + + let mut zip_iter = obj.downcast_mut::().ok_or_else(|| { + JsNativeError::typ().with_message("`this` is not a ZipIterator") + })?; + + // If already done, return { value: undefined, done: true } + if zip_iter.done { + return Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )); + } + + // Step 1: If iterCount = 0, return done. + if zip_iter.iter_count == 0 { + zip_iter.done = true; + return Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )); + } + + let mode = zip_iter.mode.clone(); + let iter_count = zip_iter.iter_count; + + let mut results: Vec = Vec::with_capacity(iter_count); + + // Step 2: For each integer i such that 0 ≤ i < iterCount, in ascending order, do + for i in 0..iter_count { + if zip_iter.iters[i].is_none() { + // iter is null → assert mode is "longest", use padding[i] + debug_assert!(mode == ZipMode::Longest); + results.push( + zip_iter + .padding + .get(i) + .cloned() + .unwrap_or(JsValue::undefined()), + ); + continue; + } + + // Let result be Completion(IteratorStepValue(iter)) + let iter = zip_iter.iters[i].as_mut().unwrap(); + let step_result = iter.step_value(context); + + match step_result { + Err(err) => { + // If result is an abrupt completion: + // Remove iter from openIters. + zip_iter.open_iters.retain(|&idx| idx != i); + zip_iter.iters[i] = None; + zip_iter.done = true; + // Return ? IteratorCloseAll(openIters, result). + let open = zip_iter.open_iters.clone(); + return Self::close_all( + &mut zip_iter.iters, + &open, + Err(err), + context, + ); + } + Ok(None) => { + // result is done. + // Remove iter from openIters. + zip_iter.open_iters.retain(|&idx| idx != i); + zip_iter.iters[i] = None; + + match mode { + ZipMode::Shortest => { + // Return ? IteratorCloseAll(openIters, ReturnCompletion(undefined)). + zip_iter.done = true; + let open = zip_iter.open_iters.clone(); + return Self::close_all( + &mut zip_iter.iters, + &open, + Ok(JsValue::undefined()), + context, + ) + .and_then(|_| { + Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )) + }); + } + ZipMode::Strict => { + if i != 0 { + // If i ≠ 0, throw TypeError after closing all. + zip_iter.done = true; + let open = zip_iter.open_iters.clone(); + let _ = Self::close_all( + &mut zip_iter.iters, + &open, + Ok(JsValue::undefined()), + context, + ); + return Err(JsNativeError::typ() + .with_message( + "iterators have different lengths in strict mode", + ) + .into()); + } + + // i == 0: Check that all remaining iterators are also done. + for k in 1..iter_count { + if zip_iter.iters[k].is_none() { + continue; + } + let other = zip_iter.iters[k].as_mut().unwrap(); + let step = other.step(context); + match step { + Err(err) => { + zip_iter.open_iters.retain(|&idx| idx != k); + zip_iter.iters[k] = None; + zip_iter.done = true; + let open = zip_iter.open_iters.clone(); + return Self::close_all( + &mut zip_iter.iters, + &open, + Err(err), + context, + ); + } + Ok(is_done) => { + if is_done { + // done → remove from openIters + zip_iter.open_iters.retain(|&idx| idx != k); + zip_iter.iters[k] = None; + } else { + // Not done → length mismatch, throw TypeError + zip_iter.done = true; + let open = zip_iter.open_iters.clone(); + let _ = Self::close_all( + &mut zip_iter.iters, + &open, + Ok(JsValue::undefined()), + context, + ); + return Err(JsNativeError::typ() + .with_message( + "iterators have different lengths in strict mode", + ) + .into()); + } + } + } + } + // All done → return done. + zip_iter.done = true; + return Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )); + } + ZipMode::Longest => { + // If openIters is empty, return done. + if zip_iter.open_iters.is_empty() { + zip_iter.done = true; + return Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )); + } + // Set iters[i] to null, use padding[i]. + results.push( + zip_iter + .padding + .get(i) + .cloned() + .unwrap_or(JsValue::undefined()), + ); + } + } + } + Ok(Some(value)) => { + results.push(value); + } + } + } + + // finishResults(results) + let result_kind = zip_iter.result_kind.clone(); + let finished = Self::finish_results(&results, &result_kind, context); + + // Yield(results) → return { value: results, done: false } + Ok(create_iter_result_object(finished, false, context)) + } + + /// `%ZipIteratorPrototype%.return()` + /// + /// Closes all underlying iterators. + pub(crate) fn r#return( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + let obj = this.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("`this` is not a ZipIterator") + })?; + + let mut zip_iter = obj.downcast_mut::().ok_or_else(|| { + JsNativeError::typ().with_message("`this` is not a ZipIterator") + })?; + + zip_iter.done = true; + let open = zip_iter.open_iters.clone(); + Self::close_all( + &mut zip_iter.iters, + &open, + Ok(JsValue::undefined()), + context, + )?; + zip_iter.open_iters.clear(); + + Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )) + } +} + +impl IntrinsicObject for ZipIterator { + fn init(realm: &Realm) { + BuiltInBuilder::with_intrinsic::(realm) + .prototype( + realm + .intrinsics() + .objects() + .iterator_prototypes() + .iterator(), + ) + .static_method(Self::next, js_string!("next"), 0) + .static_method(Self::r#return, js_string!("return"), 0) + .static_property( + JsSymbol::to_string_tag(), + js_string!("Iterator Helper"), + Attribute::CONFIGURABLE, + ) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + intrinsics.objects().iterator_prototypes().iterator() + } +} From 03cc3fa8656ab6a325283298d838f574569d5928 Mon Sep 17 00:00:00 2001 From: yush-1018 Date: Wed, 11 Mar 2026 01:16:43 +0530 Subject: [PATCH 02/12] fix(builtins): address PR feedback for Iterator.zip --- core/engine/src/builtins/iterable/mod.rs | 96 +++++++++++++------ .../src/builtins/iterable/zip_iterator.rs | 50 +++++----- 2 files changed, 91 insertions(+), 55 deletions(-) diff --git a/core/engine/src/builtins/iterable/mod.rs b/core/engine/src/builtins/iterable/mod.rs index c4def5ec797..18a534e0b6f 100644 --- a/core/engine/src/builtins/iterable/mod.rs +++ b/core/engine/src/builtins/iterable/mod.rs @@ -225,23 +225,27 @@ impl Iterator { JsNativeError::typ().with_message("Iterator.zip requires an iterable object") })?; - // 2-5. Parse mode from options (default "shortest"). + // 2. Set options to ? GetOptionsObject(options). + // 3. Let mode be ? Get(options, "mode"). + // 4. If mode is undefined, set mode to "shortest". + // 5. If mode is not one of "shortest", "longest", or "strict", throw a TypeError exception. let mode = Self::parse_zip_mode(options, context)?; - // 6-7. Parse padding option (only for "longest" mode). + // 6. Let paddingOption be undefined. + // 7. If mode is "longest", then + // a. Set paddingOption to ? Get(options, "padding"). + // b. If paddingOption is not undefined and paddingOption is not an Object, throw a TypeError exception. let padding_option = if mode == ZipMode::Longest { - let opts_obj = options.as_object(); - if let Some(opts) = opts_obj { + if let Some(opts) = options.as_object() { let p = opts.get(js_string!("padding"), context)?; - if !p.is_undefined() { - if !p.is_object() { - return Err(JsNativeError::typ() - .with_message("padding must be an object") - .into()); - } - Some(p) - } else { + if p.is_undefined() { None + } else if !p.is_object() { + return Err(JsNativeError::typ() + .with_message("padding must be an object") + .into()); + } else { + Some(p) } } else { None @@ -250,11 +254,24 @@ impl Iterator { None }; - // 8-11. Collect iterator records from iterables. + // 8. Let iters be a new empty List. + let mut iters: Vec = Vec::new(); + + // 9. Let padding be a new empty List. + // (padding list built later in build_padding) + + // 10. Let inputIter be ? GetIterator(iterables, sync). let iterables_val: JsValue = iterables_obj.clone().into(); let mut input_iter = iterables_val.get_iterator(IteratorHint::Sync, context)?; - let mut iters: Vec = Vec::new(); + // 11. Let next be not-started. + // 12. Repeat, while next is not done, + // a. Set next to Completion(IteratorStepValue(inputIter)). + // b. IfAbruptCloseIterators(next, iters). + // c. If next is not done, then + // i. Let iter be Completion(GetIteratorFlattenable(next, reject-primitives)). + // ii. IfAbruptCloseIterators(iter, the list-concatenation of « inputIter » and iters). + // iii. Append iter to iters. loop { let next = input_iter.step_value(context); match next { @@ -293,12 +310,14 @@ impl Iterator { } } + // 13. Let iterCount be the number of elements in iters. let iter_count = iters.len(); - // 12-16. Build padding list. + // 14. If mode is "longest", then ... Build padding list. let padding = Self::build_padding(padding_option, iter_count, &iters, context)?; - // Return IteratorZip(iters, mode, padding, finishResults). + // 15. Let finishResults be a new Abstract Closure ... (handled in ZipIterator::create_zip_iterator) + // 16. Return ? IteratorZip(iters, mode, padding, finishResults). Ok(ZipIterator::create_zip_iterator( iters, mode, @@ -323,23 +342,27 @@ impl Iterator { JsNativeError::typ().with_message("Iterator.zipKeyed requires an object") })?; - // 2-5. Parse mode from options. + // 2. Set options to ? GetOptionsObject(options). + // 3. Let mode be ? Get(options, "mode"). + // 4. If mode is undefined, set mode to "shortest". + // 5. If mode is not one of "shortest", "longest", or "strict", throw a TypeError exception. let mode = Self::parse_zip_mode(options, context)?; - // 6-7. Parse padding option. + // 6. Let paddingOption be undefined. + // 7. If mode is "longest", then + // a. Set paddingOption to ? Get(options, "padding"). + // b. If paddingOption is not undefined and paddingOption is not an Object, throw a TypeError exception. let padding_option = if mode == ZipMode::Longest { - let opts_obj = options.as_object(); - if let Some(opts) = opts_obj { + if let Some(opts) = options.as_object() { let p = opts.get(js_string!("padding"), context)?; - if !p.is_undefined() { - if !p.is_object() { - return Err(JsNativeError::typ() - .with_message("padding must be an object") - .into()); - } - Some(p) - } else { + if p.is_undefined() { None + } else if !p.is_object() { + return Err(JsNativeError::typ() + .with_message("padding must be an object") + .into()); + } else { + Some(p) } } else { None @@ -348,11 +371,20 @@ impl Iterator { None }; - // 8-10. Get own enumerable string-keyed properties and their iterator values. + // 8. Let iters be a new empty List. let mut iters: Vec = Vec::new(); + // 9. Let keys be a new empty List. let mut keys: Vec = Vec::new(); + // 10. Let iterablesKeys be ? EnumerableOwnProperties(iterables, key). let all_keys = iterables_obj.own_property_keys(context)?; + // 11. For each element key of iterablesKeys, do + // a. Let value be ? Get(iterables, key). + // b. If value is not undefined, then + // i. Append key to keys. + // ii. Let iter be Completion(GetIteratorFlattenable(value, reject-primitives)). + // iii. IfAbruptCloseIterators(iter, iters). + // iv. Append iter to iters. for key in all_keys { let key_val: JsValue = key.clone().into(); let value = iterables_obj.get(key.clone(), context)?; @@ -379,9 +411,11 @@ impl Iterator { } } + // 12. Let iterCount be the number of elements in iters. let iter_count = iters.len(); - // Build padding for zipKeyed. + // 13. Let padding be a new empty List. + // 14. If mode is "longest", then ... (Build padding for zipKeyed) let padding = if mode == ZipMode::Longest { match padding_option { None => vec![JsValue::undefined(); iter_count], @@ -401,6 +435,8 @@ impl Iterator { Vec::new() }; + // 15. Let finishResults be a new Abstract Closure ... (handled in ZipIterator::create_zip_iterator) + // 16. Return ? IteratorZip(iters, mode, padding, finishResults). Ok(ZipIterator::create_zip_iterator( iters, mode, diff --git a/core/engine/src/builtins/iterable/zip_iterator.rs b/core/engine/src/builtins/iterable/zip_iterator.rs index 8f6eb8644e5..6d654cded41 100644 --- a/core/engine/src/builtins/iterable/zip_iterator.rs +++ b/core/engine/src/builtins/iterable/zip_iterator.rs @@ -77,6 +77,31 @@ pub(crate) struct ZipIterator { done: bool, } +impl IntrinsicObject for ZipIterator { + fn init(realm: &Realm) { + BuiltInBuilder::with_intrinsic::(realm) + .prototype( + realm + .intrinsics() + .objects() + .iterator_prototypes() + .iterator(), + ) + .static_method(Self::next, js_string!("next"), 0) + .static_method(Self::r#return, js_string!("return"), 0) + .static_property( + JsSymbol::to_string_tag(), + js_string!("Iterator Helper"), + Attribute::CONFIGURABLE, + ) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + intrinsics.objects().iterator_prototypes().iterator() + } +} + impl ZipIterator { /// Creates a new `ZipIterator`. pub(crate) fn new( @@ -406,28 +431,3 @@ impl ZipIterator { )) } } - -impl IntrinsicObject for ZipIterator { - fn init(realm: &Realm) { - BuiltInBuilder::with_intrinsic::(realm) - .prototype( - realm - .intrinsics() - .objects() - .iterator_prototypes() - .iterator(), - ) - .static_method(Self::next, js_string!("next"), 0) - .static_method(Self::r#return, js_string!("return"), 0) - .static_property( - JsSymbol::to_string_tag(), - js_string!("Iterator Helper"), - Attribute::CONFIGURABLE, - ) - .build(); - } - - fn get(intrinsics: &Intrinsics) -> JsObject { - intrinsics.objects().iterator_prototypes().iterator() - } -} From 9f4962f8baeae35d9025a69b5945cd23781b77fc Mon Sep 17 00:00:00 2001 From: yush-1018 Date: Tue, 17 Mar 2026 14:26:28 +0530 Subject: [PATCH 03/12] refactor: gate Iterator.zip/zipKeyed behind experimental feature --- core/engine/src/builtins/iterable/mod.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/core/engine/src/builtins/iterable/mod.rs b/core/engine/src/builtins/iterable/mod.rs index 18a534e0b6f..27d26182c19 100644 --- a/core/engine/src/builtins/iterable/mod.rs +++ b/core/engine/src/builtins/iterable/mod.rs @@ -22,7 +22,9 @@ mod tests; pub(crate) use async_from_sync_iterator::AsyncFromSyncIterator; +#[cfg(feature = "experimental")] mod zip_iterator; +#[cfg(feature = "experimental")] pub(crate) use zip_iterator::{ZipIterator, ZipMode, ZipResultKind}; /// `IfAbruptCloseIterator ( value, iteratorRecord )` @@ -197,11 +199,15 @@ pub(crate) struct Iterator; impl IntrinsicObject for Iterator { fn init(realm: &Realm) { - BuiltInBuilder::with_intrinsic::(realm) - .static_method(|v, _, _| Ok(v.clone()), JsSymbol::iterator(), 0) + let builder = BuiltInBuilder::with_intrinsic::(realm) + .static_method(|v, _, _| Ok(v.clone()), JsSymbol::iterator(), 0); + + #[cfg(feature = "experimental")] + let builder = builder .static_method(Self::zip, js_string!("zip"), 1) - .static_method(Self::zip_keyed, js_string!("zipKeyed"), 1) - .build(); + .static_method(Self::zip_keyed, js_string!("zipKeyed"), 1); + + builder.build(); } fn get(intrinsics: &Intrinsics) -> JsObject { @@ -210,6 +216,7 @@ impl IntrinsicObject for Iterator { } impl Iterator { + #[cfg(feature = "experimental")] /// `Iterator.zip ( iterables [ , options ] )` /// /// More information: @@ -327,6 +334,7 @@ impl Iterator { )) } + #[cfg(feature = "experimental")] /// `Iterator.zipKeyed ( iterables [ , options ] )` /// /// More information: @@ -446,6 +454,7 @@ impl Iterator { )) } + #[cfg(feature = "experimental")] /// Parses the `mode` option from the options object. fn parse_zip_mode(options: &JsValue, context: &mut Context) -> JsResult { if options.is_undefined() || options.is_null() { @@ -469,6 +478,7 @@ impl Iterator { } } + #[cfg(feature = "experimental")] /// Builds the padding list for "longest" mode. fn build_padding( padding_option: Option, From c859d5097b0100060d2b14f99f6774d6c5d58c57 Mon Sep 17 00:00:00 2001 From: yush-1018 Date: Tue, 17 Mar 2026 23:04:02 +0530 Subject: [PATCH 04/12] fix: resolve CI failures by addressing clippy, fmt, and missing intrinsic --- core/engine/src/builtins/iterable/mod.rs | 79 ++++++++++++------- .../src/builtins/iterable/zip_iterator.rs | 54 ++++++------- core/engine/src/builtins/mod.rs | 2 + 3 files changed, 76 insertions(+), 59 deletions(-) diff --git a/core/engine/src/builtins/iterable/mod.rs b/core/engine/src/builtins/iterable/mod.rs index 27d26182c19..c56c1cd47f9 100644 --- a/core/engine/src/builtins/iterable/mod.rs +++ b/core/engine/src/builtins/iterable/mod.rs @@ -1,7 +1,7 @@ //! Boa's implementation of ECMAScript's `IteratorRecord` and iterator prototype objects. use crate::{ - Context, JsArgs, JsResult, JsValue, + Context, JsResult, JsValue, builtins::{BuiltInBuilder, IntrinsicObject}, context::intrinsics::Intrinsics, error::JsNativeError, @@ -12,6 +12,9 @@ use crate::{ }; use boa_gc::{Finalize, Trace}; +#[cfg(feature = "experimental")] +use crate::JsArgs; + mod async_from_sync_iterator; pub(crate) mod iterator_constructor; pub(crate) mod iterator_helper; @@ -88,6 +91,10 @@ pub struct IteratorPrototypes { /// The `%WrapForValidIteratorPrototype%` prototype object. wrap_for_valid_iterator: JsObject, + + /// The `ZipIteratorPrototype` prototype object. + #[cfg(feature = "experimental")] + zip_iterator: JsObject, } impl Default for IteratorPrototypes { @@ -105,6 +112,8 @@ impl Default for IteratorPrototypes { segment: JsObject::with_null_proto(), iterator_helper: JsObject::with_null_proto(), wrap_for_valid_iterator: JsObject::with_null_proto(), + #[cfg(feature = "experimental")] + zip_iterator: JsObject::with_null_proto(), } } } @@ -187,6 +196,14 @@ impl IteratorPrototypes { pub fn wrap_for_valid_iterator(&self) -> JsObject { self.wrap_for_valid_iterator.clone() } + + /// Returns the `ZipIteratorPrototype` object. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub fn zip_iterator(&self) -> JsObject { + self.zip_iterator.clone() + } } /// `%IteratorPrototype%` object @@ -199,15 +216,20 @@ pub(crate) struct Iterator; impl IntrinsicObject for Iterator { fn init(realm: &Realm) { - let builder = BuiltInBuilder::with_intrinsic::(realm) - .static_method(|v, _, _| Ok(v.clone()), JsSymbol::iterator(), 0); + let builder = BuiltInBuilder::with_intrinsic::(realm).static_method( + |v, _, _| Ok(v.clone()), + JsSymbol::iterator(), + 0, + ); + + #[cfg(not(feature = "experimental"))] + builder.build(); #[cfg(feature = "experimental")] - let builder = builder + builder .static_method(Self::zip, js_string!("zip"), 1) - .static_method(Self::zip_keyed, js_string!("zipKeyed"), 1); - - builder.build(); + .static_method(Self::zip_keyed, js_string!("zipKeyed"), 1) + .build(); } fn get(intrinsics: &Intrinsics) -> JsObject { @@ -285,7 +307,7 @@ impl Iterator { Err(err) => { // IfAbruptCloseIterators(next, iters) for iter in &iters { - let _ = iter.close(Ok(JsValue::undefined()), context); + drop(iter.close(Ok(JsValue::undefined()), context)); } return Err(err); } @@ -295,9 +317,9 @@ impl Iterator { if !value.is_object() { // Close all collected iterators and the input iterator. for iter in &iters { - let _ = iter.close(Ok(JsValue::undefined()), context); + drop(iter.close(Ok(JsValue::undefined()), context)); } - let _ = input_iter.close(Ok(JsValue::undefined()), context); + drop(input_iter.close(Ok(JsValue::undefined()), context)); return Err(JsNativeError::typ() .with_message("iterator value is not an object") .into()); @@ -306,9 +328,9 @@ impl Iterator { match iter_result { Err(err) => { for iter in &iters { - let _ = iter.close(Ok(JsValue::undefined()), context); + drop(iter.close(Ok(JsValue::undefined()), context)); } - let _ = input_iter.close(Ok(JsValue::undefined()), context); + drop(input_iter.close(Ok(JsValue::undefined()), context)); return Err(err); } Ok(iter) => iters.push(iter), @@ -400,7 +422,7 @@ impl Iterator { keys.push(key_val); if !value.is_object() { for iter in &iters { - let _ = iter.close(Ok(JsValue::undefined()), context); + drop(iter.close(Ok(JsValue::undefined()), context)); } return Err(JsNativeError::typ() .with_message("iterator value is not an object") @@ -410,7 +432,7 @@ impl Iterator { match iter { Err(err) => { for it in &iters { - let _ = it.close(Ok(JsValue::undefined()), context); + drop(it.close(Ok(JsValue::undefined()), context)); } return Err(err); } @@ -431,8 +453,7 @@ impl Iterator { let pad = pad_obj.as_object().unwrap(); let mut padding = Vec::with_capacity(iter_count); for key in &keys { - let prop_key = key.to_string(context) - .unwrap_or_default(); + let prop_key = key.to_string(context).unwrap_or_default(); let val = pad.get(prop_key, context)?; padding.push(val); } @@ -460,9 +481,9 @@ impl Iterator { if options.is_undefined() || options.is_null() { return Ok(ZipMode::Shortest); } - let opts = options.as_object().ok_or_else(|| { - JsNativeError::typ().with_message("options must be an object") - })?; + let opts = options + .as_object() + .ok_or_else(|| JsNativeError::typ().with_message("options must be an object"))?; let mode_val = opts.get(js_string!("mode"), context)?; if mode_val.is_undefined() { return Ok(ZipMode::Shortest); @@ -489,13 +510,15 @@ impl Iterator { match padding_option { None => Ok(vec![JsValue::undefined(); iter_count]), Some(pad_val) => { - let mut padding_iter = pad_val.get_iterator(IteratorHint::Sync, context) - .map_err(|err| { - for iter in iters { - let _ = iter.close(Ok(JsValue::undefined()), context); - } - err - })?; + let mut padding_iter = + pad_val + .get_iterator(IteratorHint::Sync, context) + .map_err(|err| { + for iter in iters { + drop(iter.close(Ok(JsValue::undefined()), context)); + } + err + })?; let mut padding = Vec::new(); let mut using_iterator = true; @@ -504,7 +527,7 @@ impl Iterator { match padding_iter.step_value(context) { Err(err) => { for iter in iters { - let _ = iter.close(Ok(JsValue::undefined()), context); + drop(iter.close(Ok(JsValue::undefined()), context)); } return Err(err); } @@ -525,7 +548,7 @@ impl Iterator { let close_result = padding_iter.close(Ok(JsValue::undefined()), context); if let Err(err) = close_result { for iter in iters { - let _ = iter.close(Ok(JsValue::undefined()), context); + drop(iter.close(Ok(JsValue::undefined()), context)); } return Err(err); } diff --git a/core/engine/src/builtins/iterable/zip_iterator.rs b/core/engine/src/builtins/iterable/zip_iterator.rs index 6d654cded41..b629151b0c1 100644 --- a/core/engine/src/builtins/iterable/zip_iterator.rs +++ b/core/engine/src/builtins/iterable/zip_iterator.rs @@ -5,6 +5,7 @@ //! //! [proposal]: https://tc39.es/proposal-joint-iteration/ +use crate::property::PropertyKey; use crate::{ Context, JsData, JsResult, JsValue, builtins::{ @@ -20,7 +21,6 @@ use crate::{ symbol::JsSymbol, }; use boa_gc::{Finalize, Trace}; -use crate::property::PropertyKey; /// The mode for zip iteration. #[derive(Debug, Clone, PartialEq, Eq, Trace, Finalize)] @@ -98,7 +98,7 @@ impl IntrinsicObject for ZipIterator { } fn get(intrinsics: &Intrinsics) -> JsObject { - intrinsics.objects().iterator_prototypes().iterator() + intrinsics.objects().iterator_prototypes().zip_iterator() } } @@ -139,7 +139,7 @@ impl ZipIterator { .intrinsics() .objects() .iterator_prototypes() - .iterator(), + .zip_iterator(), zip_iter, ); obj.into() @@ -180,9 +180,8 @@ impl ZipIterator { let obj = JsObject::with_null_proto(); for (i, key) in keys.iter().enumerate() { if let Some(val) = results.get(i) { - let prop_key: PropertyKey = key.to_string(context) - .unwrap_or_default() - .into(); + let prop_key: PropertyKey = + key.to_string(context).unwrap_or_default().into(); obj.set(prop_key, val.clone(), false, context) .expect("setting property on new object should not fail"); } @@ -201,13 +200,13 @@ impl ZipIterator { /// /// [proposal]: https://tc39.es/proposal-joint-iteration/#sec-iteratorzip pub(crate) fn next(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { - let obj = this.as_object().ok_or_else(|| { - JsNativeError::typ().with_message("`this` is not a ZipIterator") - })?; + let obj = this + .as_object() + .ok_or_else(|| JsNativeError::typ().with_message("`this` is not a ZipIterator"))?; - let mut zip_iter = obj.downcast_mut::().ok_or_else(|| { - JsNativeError::typ().with_message("`this` is not a ZipIterator") - })?; + let mut zip_iter = obj + .downcast_mut::() + .ok_or_else(|| JsNativeError::typ().with_message("`this` is not a ZipIterator"))?; // If already done, return { value: undefined, done: true } if zip_iter.done { @@ -261,12 +260,7 @@ impl ZipIterator { zip_iter.done = true; // Return ? IteratorCloseAll(openIters, result). let open = zip_iter.open_iters.clone(); - return Self::close_all( - &mut zip_iter.iters, - &open, - Err(err), - context, - ); + return Self::close_all(&mut zip_iter.iters, &open, Err(err), context); } Ok(None) => { // result is done. @@ -298,16 +292,14 @@ impl ZipIterator { // If i ≠ 0, throw TypeError after closing all. zip_iter.done = true; let open = zip_iter.open_iters.clone(); - let _ = Self::close_all( + drop(Self::close_all( &mut zip_iter.iters, &open, Ok(JsValue::undefined()), context, - ); + )); return Err(JsNativeError::typ() - .with_message( - "iterators have different lengths in strict mode", - ) + .with_message("iterators have different lengths in strict mode") .into()); } @@ -340,12 +332,12 @@ impl ZipIterator { // Not done → length mismatch, throw TypeError zip_iter.done = true; let open = zip_iter.open_iters.clone(); - let _ = Self::close_all( + drop(Self::close_all( &mut zip_iter.iters, &open, Ok(JsValue::undefined()), context, - ); + )); return Err(JsNativeError::typ() .with_message( "iterators have different lengths in strict mode", @@ -406,13 +398,13 @@ impl ZipIterator { _: &[JsValue], context: &mut Context, ) -> JsResult { - let obj = this.as_object().ok_or_else(|| { - JsNativeError::typ().with_message("`this` is not a ZipIterator") - })?; + let obj = this + .as_object() + .ok_or_else(|| JsNativeError::typ().with_message("`this` is not a ZipIterator"))?; - let mut zip_iter = obj.downcast_mut::().ok_or_else(|| { - JsNativeError::typ().with_message("`this` is not a ZipIterator") - })?; + let mut zip_iter = obj + .downcast_mut::() + .ok_or_else(|| JsNativeError::typ().with_message("`this` is not a ZipIterator"))?; zip_iter.done = true; let open = zip_iter.open_iters.clone(); diff --git a/core/engine/src/builtins/mod.rs b/core/engine/src/builtins/mod.rs index f17088d9776..5a8883e7ae0 100644 --- a/core/engine/src/builtins/mod.rs +++ b/core/engine/src/builtins/mod.rs @@ -254,6 +254,8 @@ impl Realm { IteratorConstructor::init(self); WrapForValidIterator::init(self); IteratorHelper::init(self); + #[cfg(feature = "experimental")] + iterable::ZipIterator::init(self); Math::init(self); Json::init(self); Array::init(self); From 4abc0d395e21a5d660311b9be2890a0c3500a114 Mon Sep 17 00:00:00 2001 From: yush-1018 Date: Wed, 18 Mar 2026 12:16:40 +0530 Subject: [PATCH 05/12] fix(lint): replace forbidden unwrap usages with expect --- core/engine/src/builtins/iterable/mod.rs | 2 +- core/engine/src/builtins/iterable/zip_iterator.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/engine/src/builtins/iterable/mod.rs b/core/engine/src/builtins/iterable/mod.rs index c56c1cd47f9..7af1f72f754 100644 --- a/core/engine/src/builtins/iterable/mod.rs +++ b/core/engine/src/builtins/iterable/mod.rs @@ -450,7 +450,7 @@ impl Iterator { match padding_option { None => vec![JsValue::undefined(); iter_count], Some(pad_obj) => { - let pad = pad_obj.as_object().unwrap(); + let pad = pad_obj.as_object().expect("padding object verification already executed above"); let mut padding = Vec::with_capacity(iter_count); for key in &keys { let prop_key = key.to_string(context).unwrap_or_default(); diff --git a/core/engine/src/builtins/iterable/zip_iterator.rs b/core/engine/src/builtins/iterable/zip_iterator.rs index b629151b0c1..d9a41bb0bc8 100644 --- a/core/engine/src/builtins/iterable/zip_iterator.rs +++ b/core/engine/src/builtins/iterable/zip_iterator.rs @@ -248,7 +248,7 @@ impl ZipIterator { } // Let result be Completion(IteratorStepValue(iter)) - let iter = zip_iter.iters[i].as_mut().unwrap(); + let iter = zip_iter.iters[i].as_mut().expect("iterator is guaranteed to be present here unless exhausted"); let step_result = iter.step_value(context); match step_result { @@ -308,7 +308,7 @@ impl ZipIterator { if zip_iter.iters[k].is_none() { continue; } - let other = zip_iter.iters[k].as_mut().unwrap(); + let other = zip_iter.iters[k].as_mut().expect("iterator is present"); let step = other.step(context); match step { Err(err) => { From b317691235c05b4d61fc68aae0915ffa6ee6262b Mon Sep 17 00:00:00 2001 From: yush-1018 Date: Wed, 18 Mar 2026 12:27:46 +0530 Subject: [PATCH 06/12] style: run cargo fmt --- core/engine/src/builtins/iterable/mod.rs | 4 +++- core/engine/src/builtins/iterable/zip_iterator.rs | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/core/engine/src/builtins/iterable/mod.rs b/core/engine/src/builtins/iterable/mod.rs index 7af1f72f754..225348984bf 100644 --- a/core/engine/src/builtins/iterable/mod.rs +++ b/core/engine/src/builtins/iterable/mod.rs @@ -450,7 +450,9 @@ impl Iterator { match padding_option { None => vec![JsValue::undefined(); iter_count], Some(pad_obj) => { - let pad = pad_obj.as_object().expect("padding object verification already executed above"); + let pad = pad_obj + .as_object() + .expect("padding object verification already executed above"); let mut padding = Vec::with_capacity(iter_count); for key in &keys { let prop_key = key.to_string(context).unwrap_or_default(); diff --git a/core/engine/src/builtins/iterable/zip_iterator.rs b/core/engine/src/builtins/iterable/zip_iterator.rs index d9a41bb0bc8..2a7b16a9fdd 100644 --- a/core/engine/src/builtins/iterable/zip_iterator.rs +++ b/core/engine/src/builtins/iterable/zip_iterator.rs @@ -248,7 +248,9 @@ impl ZipIterator { } // Let result be Completion(IteratorStepValue(iter)) - let iter = zip_iter.iters[i].as_mut().expect("iterator is guaranteed to be present here unless exhausted"); + let iter = zip_iter.iters[i] + .as_mut() + .expect("iterator is guaranteed to be present here unless exhausted"); let step_result = iter.step_value(context); match step_result { @@ -308,7 +310,8 @@ impl ZipIterator { if zip_iter.iters[k].is_none() { continue; } - let other = zip_iter.iters[k].as_mut().expect("iterator is present"); + let other = + zip_iter.iters[k].as_mut().expect("iterator is present"); let step = other.step(context); match step { Err(err) => { From 4bb9ae235e2fadd292a62b4afb63f666f8537d3f Mon Sep 17 00:00:00 2001 From: yush-1018 Date: Wed, 18 Mar 2026 12:30:24 +0530 Subject: [PATCH 07/12] fix(lint): use inspect_err instead of map_err --- core/engine/src/builtins/iterable/mod.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/core/engine/src/builtins/iterable/mod.rs b/core/engine/src/builtins/iterable/mod.rs index 225348984bf..3cfecb4745a 100644 --- a/core/engine/src/builtins/iterable/mod.rs +++ b/core/engine/src/builtins/iterable/mod.rs @@ -512,15 +512,13 @@ impl Iterator { match padding_option { None => Ok(vec![JsValue::undefined(); iter_count]), Some(pad_val) => { - let mut padding_iter = - pad_val - .get_iterator(IteratorHint::Sync, context) - .map_err(|err| { - for iter in iters { - drop(iter.close(Ok(JsValue::undefined()), context)); - } - err - })?; + let mut padding_iter = pad_val + .get_iterator(IteratorHint::Sync, context) + .inspect_err(|_err| { + for iter in iters { + drop(iter.close(Ok(JsValue::undefined()), context)); + } + })?; let mut padding = Vec::new(); let mut using_iterator = true; From bfc5183ca982502f2a07fb6c396d82b382b0160f Mon Sep 17 00:00:00 2001 From: yush-1018 Date: Wed, 18 Mar 2026 17:22:07 +0530 Subject: [PATCH 08/12] style: cargo fmt run --- core/engine/src/builtins/typed_array/tests.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/engine/src/builtins/typed_array/tests.rs b/core/engine/src/builtins/typed_array/tests.rs index a9c978c2750..99f7421c4eb 100644 --- a/core/engine/src/builtins/typed_array/tests.rs +++ b/core/engine/src/builtins/typed_array/tests.rs @@ -1,4 +1,5 @@ use crate::{JsNativeErrorKind, TestAction, run_test_actions}; +use boa_macros::js_str; #[test] fn uint8array_constructor_length() { @@ -161,3 +162,18 @@ fn typedarray_conversion_mismatch_throws() { ), ]); } + +#[test] +fn typedarray_prototype_to_locale_string() { + run_test_actions([ + TestAction::assert_eq( + "new Uint8Array([1, 2, 3]).toLocaleString()", + js_str!("1, 2, 3"), + ), + TestAction::assert_eq( + "new Float64Array([1.5, 2.5]).toLocaleString()", + js_str!("1.5, 2.5"), + ), + TestAction::assert_eq("new Uint8Array([]).toLocaleString()", js_str!("")), + ]); +} From 0977571807e9c86e9536900e100f34e0f478b1c4 Mon Sep 17 00:00:00 2001 From: yush-1018 Date: Wed, 18 Mar 2026 18:21:27 +0530 Subject: [PATCH 09/12] fix: resolve clippy redundant closure and use cow_utils for MSRV --- core/engine/src/builtins/iterable/zip_iterator.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/core/engine/src/builtins/iterable/zip_iterator.rs b/core/engine/src/builtins/iterable/zip_iterator.rs index 2a7b16a9fdd..369629b2631 100644 --- a/core/engine/src/builtins/iterable/zip_iterator.rs +++ b/core/engine/src/builtins/iterable/zip_iterator.rs @@ -281,12 +281,8 @@ impl ZipIterator { Ok(JsValue::undefined()), context, ) - .and_then(|_| { - Ok(create_iter_result_object( - JsValue::undefined(), - true, - context, - )) + .map(|_| { + create_iter_result_object(JsValue::undefined(), true, context) }); } ZipMode::Strict => { From db691c85e8bd4060231e6e7b3d6dcb5d05f07c69 Mon Sep 17 00:00:00 2001 From: yush-1018 Date: Thu, 19 Mar 2026 03:31:34 +0530 Subject: [PATCH 10/12] fix(clippy): resolve ptr_arg and doc_markdown lints --- core/engine/src/builtins/iterable/zip_iterator.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/engine/src/builtins/iterable/zip_iterator.rs b/core/engine/src/builtins/iterable/zip_iterator.rs index 369629b2631..67572fbe4b5 100644 --- a/core/engine/src/builtins/iterable/zip_iterator.rs +++ b/core/engine/src/builtins/iterable/zip_iterator.rs @@ -147,7 +147,7 @@ impl ZipIterator { /// Closes all open iterators with the given completion. fn close_all( - iters: &mut Vec>, + iters: &mut [Option], open_iters: &[usize], completion: JsResult, context: &mut Context, @@ -193,7 +193,7 @@ impl ZipIterator { /// `%ZipIteratorPrototype%.next()` /// - /// Implements the IteratorZip abstract operation from the TC39 Joint Iteration proposal. + /// Implements the `IteratorZip` abstract operation from the TC39 Joint Iteration proposal. /// /// More information: /// - [TC39 proposal][proposal] From 293b797b912ead4b3bbde5f62e0a8aba5614dc50 Mon Sep 17 00:00:00 2001 From: yush-1018 Date: Thu, 19 Mar 2026 11:45:49 +0530 Subject: [PATCH 11/12] fix: move Iterator.zip/zipKeyed to constructor, fix ZipIterator prototype chain, add tests --- .../builtins/iterable/iterator_constructor.rs | 343 ++++++++++++++++- core/engine/src/builtins/iterable/mod.rs | 345 +---------------- core/engine/src/builtins/iterable/tests.rs | 357 ++++++++++++++++++ .../src/builtins/iterable/zip_iterator.rs | 6 +- 4 files changed, 709 insertions(+), 342 deletions(-) diff --git a/core/engine/src/builtins/iterable/iterator_constructor.rs b/core/engine/src/builtins/iterable/iterator_constructor.rs index dbd173c81e2..002684cba1d 100644 --- a/core/engine/src/builtins/iterable/iterator_constructor.rs +++ b/core/engine/src/builtins/iterable/iterator_constructor.rs @@ -30,6 +30,12 @@ use super::{ wrap_for_valid_iterator::WrapForValidIterator, }; +#[cfg(feature = "experimental")] +use super::{ + IteratorHint, + zip_iterator::{ZipIterator, ZipMode, ZipResultKind}, +}; + /// The `Iterator` constructor. /// /// More information: @@ -58,7 +64,7 @@ impl IntrinsicObject for IteratorConstructor { // non-enumerable get/set accessor (web-compat requirement). We use the // builder's `constructor_accessor` support so the property is part of the // shared-shape allocation rather than a post-build override. - BuiltInBuilder::from_standard_constructor::(realm) + let builder = BuiltInBuilder::from_standard_constructor::(realm) .inherits(Some( realm .intrinsics() @@ -68,7 +74,14 @@ impl IntrinsicObject for IteratorConstructor { )) // Static methods .static_method(Self::from, js_string!("from"), 1) - .static_method(Self::concat, js_string!("concat"), 0) + .static_method(Self::concat, js_string!("concat"), 0); + + #[cfg(feature = "experimental")] + let builder = builder + .static_method(Self::zip, js_string!("zip"), 1) + .static_method(Self::zip_keyed, js_string!("zipKeyed"), 1); + + builder // Prototype methods — lazy (return IteratorHelper) .method(Self::map, js_string!("map"), 1) .method(Self::filter, js_string!("filter"), 1) @@ -105,7 +118,10 @@ impl BuiltInObject for IteratorConstructor { impl BuiltInConstructor for IteratorConstructor { const PROTOTYPE_STORAGE_SLOTS: usize = 14; // 11 methods + @@toStringTag accessor (2 slots) + constructor accessor (2 slots) + #[cfg(not(feature = "experimental"))] const CONSTRUCTOR_STORAGE_SLOTS: usize = 2; + #[cfg(feature = "experimental")] + const CONSTRUCTOR_STORAGE_SLOTS: usize = 4; const CONSTRUCTOR_ARGUMENTS: usize = 0; const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = StandardConstructors::iterator; @@ -247,6 +263,329 @@ impl IteratorConstructor { // 6. Return result. Ok(helper.into()) } + // ==================== Static Methods — Experimental ==================== + + #[cfg(feature = "experimental")] + /// `Iterator.zip ( iterables [ , options ] )` + /// + /// More information: + /// - [TC39 proposal][spec] + /// + /// [spec]: https://tc39.es/proposal-joint-iteration/#sec-iterator.zip + fn zip(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let iterables = args.get_or_undefined(0); + let options = args.get_or_undefined(1); + + // 1. If iterables is not an Object, throw a TypeError exception. + let iterables_obj = iterables.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("Iterator.zip requires an iterable object") + })?; + + // 2. Set options to ? GetOptionsObject(options). + // 3. Let mode be ? Get(options, "mode"). + // 4. If mode is undefined, set mode to "shortest". + // 5. If mode is not one of "shortest", "longest", or "strict", throw a TypeError exception. + let mode = Self::parse_zip_mode(options, context)?; + + // 6. Let paddingOption be undefined. + // 7. If mode is "longest", then + // a. Set paddingOption to ? Get(options, "padding"). + // b. If paddingOption is not undefined and paddingOption is not an Object, throw a TypeError exception. + let padding_option = if mode == ZipMode::Longest { + if let Some(opts) = options.as_object() { + let p = opts.get(js_string!("padding"), context)?; + if p.is_undefined() { + None + } else if !p.is_object() { + return Err(JsNativeError::typ() + .with_message("padding must be an object") + .into()); + } else { + Some(p) + } + } else { + None + } + } else { + None + }; + + // 8. Let iters be a new empty List. + let mut iters: Vec = Vec::new(); + + // 9. Let padding be a new empty List. + // (padding list built later in build_padding) + + // 10. Let inputIter be ? GetIterator(iterables, sync). + let iterables_val: JsValue = iterables_obj.clone().into(); + let mut input_iter = iterables_val.get_iterator(IteratorHint::Sync, context)?; + + // 11. Let next be not-started. + // 12. Repeat, while next is not done, + // a. Set next to Completion(IteratorStepValue(inputIter)). + // b. IfAbruptCloseIterators(next, iters). + // c. If next is not done, then + // i. Let iter be Completion(GetIteratorFlattenable(next, reject-primitives)). + // ii. IfAbruptCloseIterators(iter, the list-concatenation of « inputIter » and iters). + // iii. Append iter to iters. + loop { + let next = input_iter.step_value(context); + match next { + Err(err) => { + // IfAbruptCloseIterators(next, iters) + for iter in &iters { + drop(iter.close(Ok(JsValue::undefined()), context)); + } + return Err(err); + } + Ok(None) => break, // done + Ok(Some(value)) => { + // GetIteratorFlattenable(next, reject-primitives) + if !value.is_object() { + // Close all collected iterators and the input iterator. + for iter in &iters { + drop(iter.close(Ok(JsValue::undefined()), context)); + } + drop(input_iter.close(Ok(JsValue::undefined()), context)); + return Err(JsNativeError::typ() + .with_message("iterator value is not an object") + .into()); + } + let iter_result = value.get_iterator(IteratorHint::Sync, context); + match iter_result { + Err(err) => { + for iter in &iters { + drop(iter.close(Ok(JsValue::undefined()), context)); + } + drop(input_iter.close(Ok(JsValue::undefined()), context)); + return Err(err); + } + Ok(iter) => iters.push(iter), + } + } + } + } + + // 13. Let iterCount be the number of elements in iters. + let iter_count = iters.len(); + + // 14. If mode is "longest", then ... Build padding list. + let padding = Self::build_padding(padding_option, iter_count, &iters, context)?; + + // 15. Let finishResults be a new Abstract Closure ... (handled in ZipIterator::create_zip_iterator) + // 16. Return ? IteratorZip(iters, mode, padding, finishResults). + Ok(ZipIterator::create_zip_iterator( + iters, + mode, + padding, + ZipResultKind::Array, + context, + )) + } + + #[cfg(feature = "experimental")] + /// `Iterator.zipKeyed ( iterables [ , options ] )` + /// + /// More information: + /// - [TC39 proposal][spec] + /// + /// [spec]: https://tc39.es/proposal-joint-iteration/#sec-iterator.zipkeyed + fn zip_keyed(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let iterables = args.get_or_undefined(0); + let options = args.get_or_undefined(1); + + // 1. If iterables is not an Object, throw a TypeError exception. + let iterables_obj = iterables.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("Iterator.zipKeyed requires an object") + })?; + + // 2. Set options to ? GetOptionsObject(options). + // 3. Let mode be ? Get(options, "mode"). + // 4. If mode is undefined, set mode to "shortest". + // 5. If mode is not one of "shortest", "longest", or "strict", throw a TypeError exception. + let mode = Self::parse_zip_mode(options, context)?; + + // 6. Let paddingOption be undefined. + // 7. If mode is "longest", then + // a. Set paddingOption to ? Get(options, "padding"). + // b. If paddingOption is not undefined and paddingOption is not an Object, throw a TypeError exception. + let padding_option = if mode == ZipMode::Longest { + if let Some(opts) = options.as_object() { + let p = opts.get(js_string!("padding"), context)?; + if p.is_undefined() { + None + } else if !p.is_object() { + return Err(JsNativeError::typ() + .with_message("padding must be an object") + .into()); + } else { + Some(p) + } + } else { + None + } + } else { + None + }; + + // 8. Let iters be a new empty List. + let mut iters: Vec = Vec::new(); + // 9. Let keys be a new empty List. + let mut keys: Vec = Vec::new(); + + // 10. Let iterablesKeys be ? EnumerableOwnProperties(iterables, key). + let all_keys = iterables_obj.own_property_keys(context)?; + // 11. For each element key of iterablesKeys, do + // a. Let value be ? Get(iterables, key). + // b. If value is not undefined, then + // i. Append key to keys. + // ii. Let iter be Completion(GetIteratorFlattenable(value, reject-primitives)). + // iii. IfAbruptCloseIterators(iter, iters). + // iv. Append iter to iters. + for key in all_keys { + let key_val: JsValue = key.clone().into(); + let value = iterables_obj.get(key.clone(), context)?; + if !value.is_undefined() { + keys.push(key_val); + if !value.is_object() { + for iter in &iters { + drop(iter.close(Ok(JsValue::undefined()), context)); + } + return Err(JsNativeError::typ() + .with_message("iterator value is not an object") + .into()); + } + let iter = value.get_iterator(IteratorHint::Sync, context); + match iter { + Err(err) => { + for it in &iters { + drop(it.close(Ok(JsValue::undefined()), context)); + } + return Err(err); + } + Ok(iter) => iters.push(iter), + } + } + } + + // 12. Let iterCount be the number of elements in iters. + let iter_count = iters.len(); + + // 13. Let padding be a new empty List. + // 14. If mode is "longest", then ... (Build padding for zipKeyed) + let padding = if mode == ZipMode::Longest { + match padding_option { + None => vec![JsValue::undefined(); iter_count], + Some(pad_obj) => { + let pad = pad_obj + .as_object() + .expect("padding object verification already executed above"); + let mut padding = Vec::with_capacity(iter_count); + for key in &keys { + let prop_key = key.to_string(context).unwrap_or_default(); + let val = pad.get(prop_key, context)?; + padding.push(val); + } + padding + } + } + } else { + Vec::new() + }; + + // 15. Let finishResults be a new Abstract Closure ... (handled in ZipIterator::create_zip_iterator) + // 16. Return ? IteratorZip(iters, mode, padding, finishResults). + Ok(ZipIterator::create_zip_iterator( + iters, + mode, + padding, + ZipResultKind::Keyed(keys), + context, + )) + } + + #[cfg(feature = "experimental")] + /// Parses the `mode` option from the options object. + fn parse_zip_mode(options: &JsValue, context: &mut Context) -> JsResult { + if options.is_undefined() || options.is_null() { + return Ok(ZipMode::Shortest); + } + let opts = options + .as_object() + .ok_or_else(|| JsNativeError::typ().with_message("options must be an object"))?; + let mode_val = opts.get(js_string!("mode"), context)?; + if mode_val.is_undefined() { + return Ok(ZipMode::Shortest); + } + let mode_str = mode_val.to_string(context)?; + match mode_str.to_std_string_escaped().as_str() { + "shortest" => Ok(ZipMode::Shortest), + "longest" => Ok(ZipMode::Longest), + "strict" => Ok(ZipMode::Strict), + _ => Err(JsNativeError::typ() + .with_message("mode must be \"shortest\", \"longest\", or \"strict\"") + .into()), + } + } + + #[cfg(feature = "experimental")] + /// Builds the padding list for "longest" mode. + fn build_padding( + padding_option: Option, + iter_count: usize, + iters: &[super::IteratorRecord], + context: &mut Context, + ) -> JsResult> { + match padding_option { + None => Ok(vec![JsValue::undefined(); iter_count]), + Some(pad_val) => { + let mut padding_iter = pad_val + .get_iterator(IteratorHint::Sync, context) + .inspect_err(|_err| { + for iter in iters { + drop(iter.close(Ok(JsValue::undefined()), context)); + } + })?; + let mut padding = Vec::new(); + let mut using_iterator = true; + + for _ in 0..iter_count { + if using_iterator { + match padding_iter.step_value(context) { + Err(err) => { + for iter in iters { + drop(iter.close(Ok(JsValue::undefined()), context)); + } + return Err(err); + } + Ok(None) => { + using_iterator = false; + padding.push(JsValue::undefined()); + } + Ok(Some(val)) => { + padding.push(val); + } + } + } else { + padding.push(JsValue::undefined()); + } + } + + if using_iterator { + let close_result = padding_iter.close(Ok(JsValue::undefined()), context); + if let Err(err) = close_result { + for iter in iters { + drop(iter.close(Ok(JsValue::undefined()), context)); + } + return Err(err); + } + } + + Ok(padding) + } + } + } + } // ==================== Prototype Accessor Properties ==================== diff --git a/core/engine/src/builtins/iterable/mod.rs b/core/engine/src/builtins/iterable/mod.rs index 3cfecb4745a..c750b3e67aa 100644 --- a/core/engine/src/builtins/iterable/mod.rs +++ b/core/engine/src/builtins/iterable/mod.rs @@ -12,8 +12,7 @@ use crate::{ }; use boa_gc::{Finalize, Trace}; -#[cfg(feature = "experimental")] -use crate::JsArgs; + mod async_from_sync_iterator; pub(crate) mod iterator_constructor; @@ -28,7 +27,7 @@ pub(crate) use async_from_sync_iterator::AsyncFromSyncIterator; #[cfg(feature = "experimental")] mod zip_iterator; #[cfg(feature = "experimental")] -pub(crate) use zip_iterator::{ZipIterator, ZipMode, ZipResultKind}; +pub(crate) use zip_iterator::ZipIterator; /// `IfAbruptCloseIterator ( value, iteratorRecord )` /// @@ -216,19 +215,12 @@ pub(crate) struct Iterator; impl IntrinsicObject for Iterator { fn init(realm: &Realm) { - let builder = BuiltInBuilder::with_intrinsic::(realm).static_method( - |v, _, _| Ok(v.clone()), - JsSymbol::iterator(), - 0, - ); - - #[cfg(not(feature = "experimental"))] - builder.build(); - - #[cfg(feature = "experimental")] - builder - .static_method(Self::zip, js_string!("zip"), 1) - .static_method(Self::zip_keyed, js_string!("zipKeyed"), 1) + BuiltInBuilder::with_intrinsic::(realm) + .static_method( + |v, _, _| Ok(v.clone()), + JsSymbol::iterator(), + 0, + ) .build(); } @@ -237,328 +229,7 @@ impl IntrinsicObject for Iterator { } } -impl Iterator { - #[cfg(feature = "experimental")] - /// `Iterator.zip ( iterables [ , options ] )` - /// - /// More information: - /// - [TC39 proposal][spec] - /// - /// [spec]: https://tc39.es/proposal-joint-iteration/#sec-iterator.zip - fn zip(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { - let iterables = args.get_or_undefined(0); - let options = args.get_or_undefined(1); - - // 1. If iterables is not an Object, throw a TypeError exception. - let iterables_obj = iterables.as_object().ok_or_else(|| { - JsNativeError::typ().with_message("Iterator.zip requires an iterable object") - })?; - - // 2. Set options to ? GetOptionsObject(options). - // 3. Let mode be ? Get(options, "mode"). - // 4. If mode is undefined, set mode to "shortest". - // 5. If mode is not one of "shortest", "longest", or "strict", throw a TypeError exception. - let mode = Self::parse_zip_mode(options, context)?; - - // 6. Let paddingOption be undefined. - // 7. If mode is "longest", then - // a. Set paddingOption to ? Get(options, "padding"). - // b. If paddingOption is not undefined and paddingOption is not an Object, throw a TypeError exception. - let padding_option = if mode == ZipMode::Longest { - if let Some(opts) = options.as_object() { - let p = opts.get(js_string!("padding"), context)?; - if p.is_undefined() { - None - } else if !p.is_object() { - return Err(JsNativeError::typ() - .with_message("padding must be an object") - .into()); - } else { - Some(p) - } - } else { - None - } - } else { - None - }; - // 8. Let iters be a new empty List. - let mut iters: Vec = Vec::new(); - - // 9. Let padding be a new empty List. - // (padding list built later in build_padding) - - // 10. Let inputIter be ? GetIterator(iterables, sync). - let iterables_val: JsValue = iterables_obj.clone().into(); - let mut input_iter = iterables_val.get_iterator(IteratorHint::Sync, context)?; - - // 11. Let next be not-started. - // 12. Repeat, while next is not done, - // a. Set next to Completion(IteratorStepValue(inputIter)). - // b. IfAbruptCloseIterators(next, iters). - // c. If next is not done, then - // i. Let iter be Completion(GetIteratorFlattenable(next, reject-primitives)). - // ii. IfAbruptCloseIterators(iter, the list-concatenation of « inputIter » and iters). - // iii. Append iter to iters. - loop { - let next = input_iter.step_value(context); - match next { - Err(err) => { - // IfAbruptCloseIterators(next, iters) - for iter in &iters { - drop(iter.close(Ok(JsValue::undefined()), context)); - } - return Err(err); - } - Ok(None) => break, // done - Ok(Some(value)) => { - // GetIteratorFlattenable(next, reject-primitives) - if !value.is_object() { - // Close all collected iterators and the input iterator. - for iter in &iters { - drop(iter.close(Ok(JsValue::undefined()), context)); - } - drop(input_iter.close(Ok(JsValue::undefined()), context)); - return Err(JsNativeError::typ() - .with_message("iterator value is not an object") - .into()); - } - let iter_result = value.get_iterator(IteratorHint::Sync, context); - match iter_result { - Err(err) => { - for iter in &iters { - drop(iter.close(Ok(JsValue::undefined()), context)); - } - drop(input_iter.close(Ok(JsValue::undefined()), context)); - return Err(err); - } - Ok(iter) => iters.push(iter), - } - } - } - } - - // 13. Let iterCount be the number of elements in iters. - let iter_count = iters.len(); - - // 14. If mode is "longest", then ... Build padding list. - let padding = Self::build_padding(padding_option, iter_count, &iters, context)?; - - // 15. Let finishResults be a new Abstract Closure ... (handled in ZipIterator::create_zip_iterator) - // 16. Return ? IteratorZip(iters, mode, padding, finishResults). - Ok(ZipIterator::create_zip_iterator( - iters, - mode, - padding, - ZipResultKind::Array, - context, - )) - } - - #[cfg(feature = "experimental")] - /// `Iterator.zipKeyed ( iterables [ , options ] )` - /// - /// More information: - /// - [TC39 proposal][spec] - /// - /// [spec]: https://tc39.es/proposal-joint-iteration/#sec-iterator.zipkeyed - fn zip_keyed(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { - let iterables = args.get_or_undefined(0); - let options = args.get_or_undefined(1); - - // 1. If iterables is not an Object, throw a TypeError exception. - let iterables_obj = iterables.as_object().ok_or_else(|| { - JsNativeError::typ().with_message("Iterator.zipKeyed requires an object") - })?; - - // 2. Set options to ? GetOptionsObject(options). - // 3. Let mode be ? Get(options, "mode"). - // 4. If mode is undefined, set mode to "shortest". - // 5. If mode is not one of "shortest", "longest", or "strict", throw a TypeError exception. - let mode = Self::parse_zip_mode(options, context)?; - - // 6. Let paddingOption be undefined. - // 7. If mode is "longest", then - // a. Set paddingOption to ? Get(options, "padding"). - // b. If paddingOption is not undefined and paddingOption is not an Object, throw a TypeError exception. - let padding_option = if mode == ZipMode::Longest { - if let Some(opts) = options.as_object() { - let p = opts.get(js_string!("padding"), context)?; - if p.is_undefined() { - None - } else if !p.is_object() { - return Err(JsNativeError::typ() - .with_message("padding must be an object") - .into()); - } else { - Some(p) - } - } else { - None - } - } else { - None - }; - - // 8. Let iters be a new empty List. - let mut iters: Vec = Vec::new(); - // 9. Let keys be a new empty List. - let mut keys: Vec = Vec::new(); - - // 10. Let iterablesKeys be ? EnumerableOwnProperties(iterables, key). - let all_keys = iterables_obj.own_property_keys(context)?; - // 11. For each element key of iterablesKeys, do - // a. Let value be ? Get(iterables, key). - // b. If value is not undefined, then - // i. Append key to keys. - // ii. Let iter be Completion(GetIteratorFlattenable(value, reject-primitives)). - // iii. IfAbruptCloseIterators(iter, iters). - // iv. Append iter to iters. - for key in all_keys { - let key_val: JsValue = key.clone().into(); - let value = iterables_obj.get(key.clone(), context)?; - if !value.is_undefined() { - keys.push(key_val); - if !value.is_object() { - for iter in &iters { - drop(iter.close(Ok(JsValue::undefined()), context)); - } - return Err(JsNativeError::typ() - .with_message("iterator value is not an object") - .into()); - } - let iter = value.get_iterator(IteratorHint::Sync, context); - match iter { - Err(err) => { - for it in &iters { - drop(it.close(Ok(JsValue::undefined()), context)); - } - return Err(err); - } - Ok(iter) => iters.push(iter), - } - } - } - - // 12. Let iterCount be the number of elements in iters. - let iter_count = iters.len(); - - // 13. Let padding be a new empty List. - // 14. If mode is "longest", then ... (Build padding for zipKeyed) - let padding = if mode == ZipMode::Longest { - match padding_option { - None => vec![JsValue::undefined(); iter_count], - Some(pad_obj) => { - let pad = pad_obj - .as_object() - .expect("padding object verification already executed above"); - let mut padding = Vec::with_capacity(iter_count); - for key in &keys { - let prop_key = key.to_string(context).unwrap_or_default(); - let val = pad.get(prop_key, context)?; - padding.push(val); - } - padding - } - } - } else { - Vec::new() - }; - - // 15. Let finishResults be a new Abstract Closure ... (handled in ZipIterator::create_zip_iterator) - // 16. Return ? IteratorZip(iters, mode, padding, finishResults). - Ok(ZipIterator::create_zip_iterator( - iters, - mode, - padding, - ZipResultKind::Keyed(keys), - context, - )) - } - - #[cfg(feature = "experimental")] - /// Parses the `mode` option from the options object. - fn parse_zip_mode(options: &JsValue, context: &mut Context) -> JsResult { - if options.is_undefined() || options.is_null() { - return Ok(ZipMode::Shortest); - } - let opts = options - .as_object() - .ok_or_else(|| JsNativeError::typ().with_message("options must be an object"))?; - let mode_val = opts.get(js_string!("mode"), context)?; - if mode_val.is_undefined() { - return Ok(ZipMode::Shortest); - } - let mode_str = mode_val.to_string(context)?; - match mode_str.to_std_string_escaped().as_str() { - "shortest" => Ok(ZipMode::Shortest), - "longest" => Ok(ZipMode::Longest), - "strict" => Ok(ZipMode::Strict), - _ => Err(JsNativeError::typ() - .with_message("mode must be \"shortest\", \"longest\", or \"strict\"") - .into()), - } - } - - #[cfg(feature = "experimental")] - /// Builds the padding list for "longest" mode. - fn build_padding( - padding_option: Option, - iter_count: usize, - iters: &[IteratorRecord], - context: &mut Context, - ) -> JsResult> { - match padding_option { - None => Ok(vec![JsValue::undefined(); iter_count]), - Some(pad_val) => { - let mut padding_iter = pad_val - .get_iterator(IteratorHint::Sync, context) - .inspect_err(|_err| { - for iter in iters { - drop(iter.close(Ok(JsValue::undefined()), context)); - } - })?; - let mut padding = Vec::new(); - let mut using_iterator = true; - - for _ in 0..iter_count { - if using_iterator { - match padding_iter.step_value(context) { - Err(err) => { - for iter in iters { - drop(iter.close(Ok(JsValue::undefined()), context)); - } - return Err(err); - } - Ok(None) => { - using_iterator = false; - padding.push(JsValue::undefined()); - } - Ok(Some(val)) => { - padding.push(val); - } - } - } else { - padding.push(JsValue::undefined()); - } - } - - if using_iterator { - let close_result = padding_iter.close(Ok(JsValue::undefined()), context); - if let Err(err) = close_result { - for iter in iters { - drop(iter.close(Ok(JsValue::undefined()), context)); - } - return Err(err); - } - } - - Ok(padding) - } - } - } -} /// `%AsyncIteratorPrototype%` object /// diff --git a/core/engine/src/builtins/iterable/tests.rs b/core/engine/src/builtins/iterable/tests.rs index bac4bdab048..976afbef2c6 100644 --- a/core/engine/src/builtins/iterable/tests.rs +++ b/core/engine/src/builtins/iterable/tests.rs @@ -375,6 +375,44 @@ fn iterator_concat_zero_arguments() { )]); } +// ── Iterator.zip — shortest mode (default) ────────────────────────────────── + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_basic_two_arrays() { + run_test_actions([TestAction::assert_eq( + "JSON.stringify(Iterator.zip([[1,2,3], ['a','b','c']]).toArray())", + js_str!("[[1,\"a\"],[2,\"b\"],[3,\"c\"]]"), + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_basic_three_arrays() { + run_test_actions([TestAction::assert_eq( + "JSON.stringify(Iterator.zip([[1,2], ['a','b'], [true, false]]).toArray())", + js_str!("[[1,\"a\",true],[2,\"b\",false]]"), + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_stops_at_shortest() { + run_test_actions([TestAction::assert_eq( + "JSON.stringify(Iterator.zip([[1,2,3], ['a']]).toArray())", + js_str!("[[1,\"a\"]]"), + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_empty_iterables() { + run_test_actions([TestAction::assert_eq( + "Iterator.zip([]).toArray().length", + 0, + )]); +} + #[test] fn iterator_concat_single_argument() { run_test_actions([TestAction::assert_eq( @@ -399,6 +437,130 @@ fn iterator_concat_lazy_next() { )]); } +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_single_iterable() { + run_test_actions([TestAction::assert_eq( + "JSON.stringify(Iterator.zip([[1,2,3]]).toArray())", + js_str!("[[1],[2],[3]]"), + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_shortest_mode_explicit() { + run_test_actions([TestAction::assert_eq( + "JSON.stringify(Iterator.zip([[1,2,3], ['a','b']], { mode: 'shortest' }).toArray())", + js_str!("[[1,\"a\"],[2,\"b\"]]"), + )]); +} + +// ── Iterator.zip — longest mode ───────────────────────────────────────────── + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_longest_pads_with_undefined() { + run_test_actions([TestAction::assert_eq( + r#" + const result = Iterator.zip([[1,2,3], ['a']], { mode: 'longest' }).toArray(); + JSON.stringify(result) + "#, + js_str!("[[1,\"a\"],[2,null],[3,null]]"), + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_longest_custom_padding() { + run_test_actions([TestAction::assert_eq( + r#" + const result = Iterator.zip( + [[1,2,3], ['a']], + { mode: 'longest', padding: ['?', '!'] } + ).toArray(); + JSON.stringify(result) + "#, + js_str!("[[1,\"a\"],[2,\"!\"],[3,\"!\"]]"), + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_longest_same_length() { + run_test_actions([TestAction::assert_eq( + r#" + JSON.stringify(Iterator.zip([[1,2], ['a','b']], { mode: 'longest' }).toArray()) + "#, + js_str!("[[1,\"a\"],[2,\"b\"]]"), + )]); +} + +// ── Iterator.zip — strict mode ────────────────────────────────────────────── + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_strict_same_length() { + run_test_actions([TestAction::assert_eq( + "JSON.stringify(Iterator.zip([[1,2], ['a','b']], { mode: 'strict' }).toArray())", + js_str!("[[1,\"a\"],[2,\"b\"]]"), + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_strict_different_length_throws() { + run_test_actions([TestAction::assert_native_error( + "Iterator.zip([[1,2,3], ['a','b']], { mode: 'strict' }).toArray()", + JsNativeErrorKind::Type, + "iterators have different lengths in strict mode", + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_strict_first_shorter_throws() { + run_test_actions([TestAction::assert_native_error( + "Iterator.zip([[1], ['a','b','c']], { mode: 'strict' }).toArray()", + JsNativeErrorKind::Type, + "iterators have different lengths in strict mode", + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_strict_empty_iterators() { + run_test_actions([TestAction::assert_eq( + "Iterator.zip([[], []], { mode: 'strict' }).toArray().length", + 0, + )]); +} + +// ── Iterator.zipKeyed ─────────────────────────────────────────────────────── + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_keyed_basic() { + run_test_actions([TestAction::assert_eq( + r#" + const result = Iterator.zipKeyed({ a: [1,2,3], b: ['x','y','z'] }).toArray(); + result.map(o => o.a + ':' + o.b).join(',') + "#, + js_str!("1:x,2:y,3:z"), + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_keyed_shortest_default() { + run_test_actions([TestAction::assert_eq( + r#" + const result = Iterator.zipKeyed({ x: [1,2,3], y: ['a'] }).toArray(); + result.length + "#, + 1, + )]); +} + #[test] fn iterator_concat_non_object_throws() { run_test_actions([TestAction::assert_native_error( @@ -445,3 +607,198 @@ fn iterator_concat_return_result_shape() { const r = it.return(); r.done === true && r.value === undefined", )]); } + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_keyed_longest_mode() { + run_test_actions([TestAction::assert_eq( + r#" + const result = Iterator.zipKeyed( + { x: [1,2,3], y: ['a'] }, + { mode: 'longest' } + ).toArray(); + result.length + "#, + 3, + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_keyed_longest_with_padding() { + run_test_actions([TestAction::assert_eq( + r#" + const result = Iterator.zipKeyed( + { x: [1,2,3], y: ['a'] }, + { mode: 'longest', padding: { y: 'default' } } + ).toArray(); + result.map(o => o.x + ':' + o.y).join(',') + "#, + js_str!("1:a,2:default,3:default"), + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_keyed_strict_same_length() { + run_test_actions([TestAction::assert_eq( + r#" + const result = Iterator.zipKeyed( + { a: [1,2], b: ['x','y'] }, + { mode: 'strict' } + ).toArray(); + result.length + "#, + 2, + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_keyed_strict_different_length_throws() { + run_test_actions([TestAction::assert_native_error( + r#" + Iterator.zipKeyed( + { a: [1,2,3], b: ['x','y'] }, + { mode: 'strict' } + ).toArray() + "#, + JsNativeErrorKind::Type, + "iterators have different lengths in strict mode", + )]); +} + +// ── Error handling ────────────────────────────────────────────────────────── + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_non_object_iterables_throws() { + run_test_actions([TestAction::assert_native_error( + "Iterator.zip(42)", + JsNativeErrorKind::Type, + "Iterator.zip requires an iterable object", + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_invalid_mode_throws() { + run_test_actions([TestAction::assert_native_error( + "Iterator.zip([[1]], { mode: 'invalid' })", + JsNativeErrorKind::Type, + "mode must be \"shortest\", \"longest\", or \"strict\"", + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_non_object_padding_throws() { + run_test_actions([TestAction::assert_native_error( + "Iterator.zip([[1]], { mode: 'longest', padding: 42 })", + JsNativeErrorKind::Type, + "padding must be an object", + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_options_must_be_object() { + run_test_actions([TestAction::assert_native_error( + "Iterator.zip([[1]], 'notAnObject')", + JsNativeErrorKind::Type, + "options must be an object", + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_keyed_non_object_iterables_throws() { + run_test_actions([TestAction::assert_native_error( + "Iterator.zipKeyed(42)", + JsNativeErrorKind::Type, + "Iterator.zipKeyed requires an object", + )]); +} + +// ── ZipIterator protocol ──────────────────────────────────────────────────── + +#[cfg(feature = "experimental")] +#[test] +fn zip_iterator_return_closes_iterators() { + run_test_actions([ + TestAction::run( + r#" + let closed1 = false; + let closed2 = false; + const iter1 = { + [Symbol.iterator]() { return this; }, + next() { return { value: 1, done: false }; }, + return() { closed1 = true; return { value: undefined, done: true }; } + }; + const iter2 = { + [Symbol.iterator]() { return this; }, + next() { return { value: 2, done: false }; }, + return() { closed2 = true; return { value: undefined, done: true }; } + }; + const zipped = Iterator.zip([iter1, iter2]); + zipped.return(); + "#, + ), + TestAction::assert("closed1"), + TestAction::assert("closed2"), + ]); +} + +#[cfg(feature = "experimental")] +#[test] +fn zip_iterator_next_after_done() { + run_test_actions([ + TestAction::run( + r#" + const zipped = Iterator.zip([[]]); + "#, + ), + TestAction::assert_eq( + "JSON.stringify(zipped.next())", + js_str!("{\"done\":true}"), + ), + TestAction::assert_eq( + "JSON.stringify(zipped.next())", + js_str!("{\"done\":true}"), + ), + ]); +} + +#[cfg(feature = "experimental")] +#[test] +fn zip_iterator_to_string_tag() { + run_test_actions([TestAction::assert_eq( + "Iterator.zip([[1]]).next(); Iterator.zip([[1]])[Symbol.toStringTag]", + js_str!("Iterator Helper"), + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_with_generators() { + run_test_actions([TestAction::assert_eq( + r#" + function* nums() { yield 1; yield 2; yield 3; } + function* letters() { yield 'a'; yield 'b'; yield 'c'; } + JSON.stringify(Iterator.zip([nums(), letters()]).toArray()) + "#, + js_str!("[[1,\"a\"],[2,\"b\"],[3,\"c\"]]"), + )]); +} + +#[cfg(feature = "experimental")] +#[test] +fn iterator_zip_longest_with_three_iterators() { + run_test_actions([TestAction::assert_eq( + r#" + const result = Iterator.zip([[1], [10, 20], [100, 200, 300]], { mode: 'longest' }).toArray(); + JSON.stringify(result) + "#, + js_str!("[[1,10,100],[null,20,200],[null,null,300]]"), + )]); +} diff --git a/core/engine/src/builtins/iterable/zip_iterator.rs b/core/engine/src/builtins/iterable/zip_iterator.rs index 67572fbe4b5..c6183231ade 100644 --- a/core/engine/src/builtins/iterable/zip_iterator.rs +++ b/core/engine/src/builtins/iterable/zip_iterator.rs @@ -83,9 +83,9 @@ impl IntrinsicObject for ZipIterator { .prototype( realm .intrinsics() - .objects() - .iterator_prototypes() - .iterator(), + .constructors() + .iterator() + .prototype(), ) .static_method(Self::next, js_string!("next"), 0) .static_method(Self::r#return, js_string!("return"), 0) From b1e8393b198609caf94b910fc92e53a6478f6874 Mon Sep 17 00:00:00 2001 From: yush-1018 Date: Thu, 19 Mar 2026 12:23:34 +0530 Subject: [PATCH 12/12] style: cargo fmt run for zip iterator --- .../builtins/iterable/iterator_constructor.rs | 1 - core/engine/src/builtins/iterable/mod.rs | 10 +--------- core/engine/src/builtins/iterable/tests.rs | 10 ++-------- .../engine/src/builtins/iterable/zip_iterator.rs | 8 +------- core/engine/src/builtins/typed_array/tests.rs | 16 ---------------- 5 files changed, 4 insertions(+), 41 deletions(-) diff --git a/core/engine/src/builtins/iterable/iterator_constructor.rs b/core/engine/src/builtins/iterable/iterator_constructor.rs index 002684cba1d..a970bf867dd 100644 --- a/core/engine/src/builtins/iterable/iterator_constructor.rs +++ b/core/engine/src/builtins/iterable/iterator_constructor.rs @@ -584,7 +584,6 @@ impl IteratorConstructor { Ok(padding) } } - } } // ==================== Prototype Accessor Properties ==================== diff --git a/core/engine/src/builtins/iterable/mod.rs b/core/engine/src/builtins/iterable/mod.rs index c750b3e67aa..d912e5e806e 100644 --- a/core/engine/src/builtins/iterable/mod.rs +++ b/core/engine/src/builtins/iterable/mod.rs @@ -12,8 +12,6 @@ use crate::{ }; use boa_gc::{Finalize, Trace}; - - mod async_from_sync_iterator; pub(crate) mod iterator_constructor; pub(crate) mod iterator_helper; @@ -216,11 +214,7 @@ pub(crate) struct Iterator; impl IntrinsicObject for Iterator { fn init(realm: &Realm) { BuiltInBuilder::with_intrinsic::(realm) - .static_method( - |v, _, _| Ok(v.clone()), - JsSymbol::iterator(), - 0, - ) + .static_method(|v, _, _| Ok(v.clone()), JsSymbol::iterator(), 0) .build(); } @@ -229,8 +223,6 @@ impl IntrinsicObject for Iterator { } } - - /// `%AsyncIteratorPrototype%` object /// /// More information: diff --git a/core/engine/src/builtins/iterable/tests.rs b/core/engine/src/builtins/iterable/tests.rs index 976afbef2c6..b29acba2ba5 100644 --- a/core/engine/src/builtins/iterable/tests.rs +++ b/core/engine/src/builtins/iterable/tests.rs @@ -758,14 +758,8 @@ fn zip_iterator_next_after_done() { const zipped = Iterator.zip([[]]); "#, ), - TestAction::assert_eq( - "JSON.stringify(zipped.next())", - js_str!("{\"done\":true}"), - ), - TestAction::assert_eq( - "JSON.stringify(zipped.next())", - js_str!("{\"done\":true}"), - ), + TestAction::assert_eq("JSON.stringify(zipped.next())", js_str!("{\"done\":true}")), + TestAction::assert_eq("JSON.stringify(zipped.next())", js_str!("{\"done\":true}")), ]); } diff --git a/core/engine/src/builtins/iterable/zip_iterator.rs b/core/engine/src/builtins/iterable/zip_iterator.rs index c6183231ade..d18314aaa15 100644 --- a/core/engine/src/builtins/iterable/zip_iterator.rs +++ b/core/engine/src/builtins/iterable/zip_iterator.rs @@ -80,13 +80,7 @@ pub(crate) struct ZipIterator { impl IntrinsicObject for ZipIterator { fn init(realm: &Realm) { BuiltInBuilder::with_intrinsic::(realm) - .prototype( - realm - .intrinsics() - .constructors() - .iterator() - .prototype(), - ) + .prototype(realm.intrinsics().constructors().iterator().prototype()) .static_method(Self::next, js_string!("next"), 0) .static_method(Self::r#return, js_string!("return"), 0) .static_property( diff --git a/core/engine/src/builtins/typed_array/tests.rs b/core/engine/src/builtins/typed_array/tests.rs index 99f7421c4eb..a9c978c2750 100644 --- a/core/engine/src/builtins/typed_array/tests.rs +++ b/core/engine/src/builtins/typed_array/tests.rs @@ -1,5 +1,4 @@ use crate::{JsNativeErrorKind, TestAction, run_test_actions}; -use boa_macros::js_str; #[test] fn uint8array_constructor_length() { @@ -162,18 +161,3 @@ fn typedarray_conversion_mismatch_throws() { ), ]); } - -#[test] -fn typedarray_prototype_to_locale_string() { - run_test_actions([ - TestAction::assert_eq( - "new Uint8Array([1, 2, 3]).toLocaleString()", - js_str!("1, 2, 3"), - ), - TestAction::assert_eq( - "new Float64Array([1.5, 2.5]).toLocaleString()", - js_str!("1.5, 2.5"), - ), - TestAction::assert_eq("new Uint8Array([]).toLocaleString()", js_str!("")), - ]); -}