diff --git a/core/engine/src/builtins/iterable/mod.rs b/core/engine/src/builtins/iterable/mod.rs index bbeda81acba..3cfecb4745a 100644 --- a/core/engine/src/builtins/iterable/mod.rs +++ b/core/engine/src/builtins/iterable/mod.rs @@ -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; @@ -22,6 +25,11 @@ 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 )` /// /// `IfAbruptCloseIterator` is a shorthand for a sequence of algorithm steps that use an `Iterator` @@ -83,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 { @@ -100,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(), } } } @@ -182,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 @@ -194,8 +216,19 @@ 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(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) .build(); } @@ -204,6 +237,329 @@ 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 /// /// 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..67572fbe4b5 --- /dev/null +++ b/core/engine/src/builtins/iterable/zip_iterator.rs @@ -0,0 +1,424 @@ +//! 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::property::PropertyKey; +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}; + +/// 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 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().zip_iterator() + } +} + +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() + .zip_iterator(), + zip_iter, + ); + obj.into() + } + + /// Closes all open iterators with the given completion. + fn close_all( + iters: &mut [Option], + 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() + .expect("iterator is guaranteed to be present here unless exhausted"); + 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, + ) + .map(|_| { + 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(); + 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") + .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().expect("iterator is present"); + 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(); + 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", + ) + .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, + )) + } +} diff --git a/core/engine/src/builtins/mod.rs b/core/engine/src/builtins/mod.rs index 470e6ea399b..4f6d1920259 100644 --- a/core/engine/src/builtins/mod.rs +++ b/core/engine/src/builtins/mod.rs @@ -252,6 +252,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); diff --git a/core/runtime/src/extensions.rs b/core/runtime/src/extensions.rs index bc760264baa..f5f6bd6f977 100644 --- a/core/runtime/src/extensions.rs +++ b/core/runtime/src/extensions.rs @@ -143,6 +143,15 @@ impl RuntimeExtension { fn register(self, realm: Option, context: &mut Context) -> JsResult<()> { crate::message::register(self.0, realm, context) +} + +/// Register the `Worker` JavaScript API. +#[derive(Debug)] +pub struct WorkerExtension; + +impl RuntimeExtension for WorkerExtension { + fn register(self, realm: Option, context: &mut Context) -> JsResult<()> { + crate::worker::Worker::register(realm, context) } } diff --git a/core/runtime/src/lib.rs b/core/runtime/src/lib.rs index 9e8b22d7617..8d4bbf0b8ee 100644 --- a/core/runtime/src/lib.rs +++ b/core/runtime/src/lib.rs @@ -126,12 +126,13 @@ pub mod store; pub mod text; #[cfg(feature = "url")] pub mod url; +pub mod worker; #[cfg(feature = "process")] use crate::extensions::ProcessExtension; use crate::extensions::{ Base64Extension, EncodingExtension, MicrotaskExtension, StructuredCloneExtension, - TimeoutExtension, + TimeoutExtension, WorkerExtension, }; pub use extensions::RuntimeExtension; @@ -157,6 +158,7 @@ pub fn register( ProcessExtension, #[cfg(feature = "fetch")] extensions::AbortControllerExtension, + WorkerExtension, extensions, ) .register(realm, ctx)?; diff --git a/core/runtime/src/worker/mod.rs b/core/runtime/src/worker/mod.rs new file mode 100644 index 00000000000..7a35b3ef8d3 --- /dev/null +++ b/core/runtime/src/worker/mod.rs @@ -0,0 +1,113 @@ +//! Boa's implementation of the Web Worker API. +//! +//! The `Worker` class represents a Web Worker. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [WHATWG `Worker` specification][spec] +//! +//! [spec]: https://html.spec.whatwg.org/multipage/workers.html +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Worker + +#[cfg(test)] +pub(crate) mod tests; + +use boa_engine::class::Class; +use boa_engine::realm::Realm; +use boa_engine::value::Convert; +use boa_engine::{ + boa_class, boa_module, js_error, Context, Finalize, JsData, JsResult, JsValue, Source, Trace, +}; +use std::sync::mpsc::{channel, Sender}; +use std::thread; + +/// The `Worker` class represents a Web Worker +#[derive(Debug, Clone, JsData, Trace, Finalize)] +#[boa_gc(unsafe_no_drop)] +pub struct Worker { + #[unsafe_ignore_trace] + sender: Sender, +} + +impl Worker { + /// Register the `Worker` class into the realm. Pass `None` for the realm to + /// register globally. + pub fn register(realm: Option, context: &mut Context) -> JsResult<()> { + js_module::boa_register(realm, context) + } +} + +#[boa_class(rename = "Worker")] +#[boa(rename_all = "camelCase")] +impl Worker { + /// Create a new `Worker` object. + #[boa(constructor)] + pub fn new(url: Convert, context: &mut Context) -> JsResult { + let (tx, rx) = channel::(); + + let script = std::fs::read_to_string(&url.0) + .map_err(|e| js_error!(TypeError: "Failed to read worker script '{}': {}", url.0, e))?; + + thread::spawn(move || { + let mut worker_context = Context::default(); + // We should ideally register some runtime APIs here, like console. + // But leaving it basic for now to avoid circular dependencies in `boa_runtime`. + + // Evaluate the initial script + if let Err(e) = worker_context.eval(Source::from_bytes(&script)) { + eprintln!("Worker error: {}", e); + return; + } + + // Simple message loop + while let Ok(msg) = rx.recv() { + let global = worker_context.global_object().clone(); + if let Ok(onmessage) = + global.get(boa_engine::js_string!("onmessage"), &mut worker_context) + { + if let Some(func) = onmessage.as_callable() { + let event = + boa_engine::object::JsObject::default(&worker_context.intrinsics()); + let _ = event.set( + boa_engine::js_string!("data"), + JsValue::from(boa_engine::js_string!(msg)), + false, + &mut worker_context, + ); + let _ = func.call(&global.into(), &[event.into()], &mut worker_context); + let _ = worker_context.run_jobs(); + } + } + } + }); + + Ok(Self { sender: tx }) + } + + /// `Worker.prototype.postMessage(message)` + pub fn post_message( + &self, + message: Convert, + _context: &mut Context, + ) -> JsResult { + self.sender + .send(message.0) + .map_err(|e| js_error!(Error: "Failed to send message to worker: {}", e))?; + Ok(JsValue::undefined()) + } + + /// `Worker.prototype.terminate()` + pub fn terminate(&self, _context: &mut Context) -> JsResult { + // Since we are using an mpsc channel, dropping the sender isn't enough + // unless we replace the sender with an Option. + // For standard Web Workers, terminate stops it immediately. + // For now, this is a no-op that relies on the main process exiting. + Ok(JsValue::undefined()) + } +} + +/// JavaScript module containing the Worker class. +#[boa_module] +pub mod js_module { + type Worker = super::Worker; +} diff --git a/core/runtime/src/worker/tests.rs b/core/runtime/src/worker/tests.rs new file mode 100644 index 00000000000..ae1b59f2428 --- /dev/null +++ b/core/runtime/src/worker/tests.rs @@ -0,0 +1,36 @@ +use super::Worker; +use crate::extensions::WorkerExtension; +use crate::test::TestAction; + +#[test] +fn basic_worker_spawn() { + let script = r#" + let a = 1 + 2; + if (a !== 3) { + throw new Error("Math is broken"); + } + "#; + + // Create a temporary file for the worker script + let temp_dir = std::env::temp_dir(); + let script_path = temp_dir.join("worker_test.js"); + std::fs::write(&script_path, script).expect("Failed to write test script"); + + let actions = vec![ + TestAction::run(format!( + r#" + let worker = new Worker("{}"); + "#, + script_path.to_string_lossy().replace("\\", "\\\\") // Escape path for JS string + )), + TestAction::run(r#" + worker.postMessage("test message"); + worker.terminate(); + "#), + ]; + + crate::test::run_test_actions_with( + actions, + &mut boa_engine::Context::default(), + ); +}