From ab6a7c3ea1d2ba85f4fa28f8e62f6dbbdb2d6f96 Mon Sep 17 00:00:00 2001 From: Emile Fugulin Date: Sun, 26 May 2024 22:36:14 -0400 Subject: [PATCH 1/5] Add iterator --- core/src/lib.rs | 7 +- core/src/value.rs | 2 + core/src/value/iterator.rs | 284 +++++++++++++++++++++++++++ core/src/value/iterator/into_iter.rs | 64 ++++++ core/src/value/iterator/iterable.rs | 153 +++++++++++++++ 5 files changed, 507 insertions(+), 3 deletions(-) create mode 100644 core/src/value/iterator.rs create mode 100644 core/src/value/iterator/into_iter.rs create mode 100644 core/src/value/iterator/iterable.rs diff --git a/core/src/lib.rs b/core/src/lib.rs index 88b558cd..9f0cc653 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -24,9 +24,10 @@ pub use class::Class; pub use persistent::{Outlive, Persistent}; pub use result::{CatchResultExt, CaughtError, CaughtResult, Error, Result, ThrowResultExt}; pub use value::{ - array, atom, convert, function, module, object, promise, Array, Atom, BigInt, Coerced, - Exception, Filter, FromAtom, FromIteratorJs, FromJs, Function, IntoAtom, IntoJs, IteratorJs, - Module, Null, Object, Promise, String, Symbol, Type, Undefined, Value, + array, atom, convert, function, iterator, module, object, promise, Array, Atom, BigInt, + Coerced, Exception, Filter, FromAtom, FromIteratorJs, FromJs, Function, IntoAtom, IntoJs, + Iterable, Iterator, IteratorJs, Module, Null, Object, Promise, String, Symbol, Type, Undefined, + Value, }; #[cfg(feature = "allocator")] diff --git a/core/src/value.rs b/core/src/value.rs index 88915d06..b2ce4895 100644 --- a/core/src/value.rs +++ b/core/src/value.rs @@ -7,6 +7,7 @@ mod bigint; pub mod convert; pub(crate) mod exception; pub mod function; +pub mod iterator; pub mod module; pub mod object; pub mod promise; @@ -19,6 +20,7 @@ pub use bigint::BigInt; pub use convert::{Coerced, FromAtom, FromIteratorJs, FromJs, IntoAtom, IntoJs, IteratorJs}; pub use exception::Exception; pub use function::{Constructor, Function}; +pub use iterator::{Iterable, Iterator}; pub use module::Module; pub use object::{Filter, Object}; pub use promise::Promise; diff --git a/core/src/value/iterator.rs b/core/src/value/iterator.rs new file mode 100644 index 00000000..1388cfff --- /dev/null +++ b/core/src/value/iterator.rs @@ -0,0 +1,284 @@ +use std::ops::Deref; + +use crate::{ + atom::PredefinedAtom, + function::{Func, IntoArgs, MutFn, This}, + Ctx, Error, FromJs, Function, IntoJs, Object, Result, Value, +}; + +mod into_iter; +mod iterable; + +pub use into_iter::{IterFn, IterFnMut}; +pub use iterable::Iterable; + +/// A trait for converting a Rust object into a JavaScript iterator. +pub trait IntoJsIter<'js, I> +where + I: IntoJs<'js>, +{ + fn next(&mut self, ctx: Ctx<'js>, position: usize) -> Result>; +} + +/// A javascript iterator. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct Iterator<'js>(pub(crate) Object<'js>); + +impl<'js> Iterator<'js> { + /// Create a new iterable iterator from a Rust object which implements [`IntoJsIter`]. + /// + pub fn new(ctx: Ctx<'js>, mut it: T) -> Result + where + T: IntoJsIter<'js, I> + 'js, + I: IntoJs<'js>, + { + let iterator = Object::new(ctx.clone())?; + iterator.set("position", 0usize)?; + iterator.set( + PredefinedAtom::SymbolIterator, + Func::from(|it: This>| -> Result> { Ok(it.0) }), + )?; + iterator.set( + "next", + Function::new( + ctx, + MutFn::from( + move |ctx: Ctx<'js>, this: This>| -> Result> { + let position = this.get::<_, usize>("position")?; + let res = Object::new(ctx.clone())?; + if let Some(value) = it.next(ctx, position)? { + res.set("value", value)?; + this.set("position", position + 1)?; + } else { + res.set(PredefinedAtom::Done, true)?; + } + Ok(res) + }, + ), + ), + )?; + + Ok(Self(iterator)) + } + + /// Get the next value from the iterator. + pub fn next(&self) -> Result>> { + let next_fn = self.0.get::<_, Function>(PredefinedAtom::Next)?; + let next = (This(self.0.clone()), 2).apply::>(&next_fn)?; + if let Ok(done) = next.get::<_, bool>(PredefinedAtom::Done) { + if done { + return Ok(None); + } + } + let value = next.get::<_, Value<'_>>("value")?; + Ok(Some(value)) + } + + /// Reference to value + #[inline] + pub fn as_value(&self) -> &Value<'js> { + self.0.as_value() + } + + /// Convert into value + #[inline] + pub fn into_value(self) -> Value<'js> { + self.0.into_value() + } + + /// Convert from value + pub fn from_value(value: Value<'js>) -> Option { + Self::from_object(Object::from_value(value).ok()?) + } + + /// Reference as an object + #[inline] + pub fn as_object(&self) -> &Object<'js> { + &self.0 + } + + /// Convert into an object + #[inline] + pub fn into_object(self) -> Object<'js> { + self.0 + } + + /// Convert from an object + pub fn from_object(object: Object<'js>) -> Option { + if object.is_iterator() { + Some(Self(object)) + } else { + None + } + } +} + +impl<'js> Deref for Iterator<'js> { + type Target = Object<'js>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'js> AsRef> for Iterator<'js> { + fn as_ref(&self) -> &Object<'js> { + &self.0 + } +} + +impl<'js> AsRef> for Iterator<'js> { + fn as_ref(&self) -> &Value<'js> { + self.0.as_ref() + } +} + +impl<'js> From> for Value<'js> { + fn from(value: Iterator<'js>) -> Self { + value.into_value() + } +} + +impl<'js> FromJs<'js> for Iterator<'js> { + fn from_js(_: &Ctx<'js>, value: Value<'js>) -> Result { + let ty_name = value.type_name(); + if let Some(v) = Self::from_value(value) { + Ok(v) + } else { + Err(Error::new_from_js(ty_name, "Iterator")) + } + } +} + +impl<'js> IntoJs<'js> for Iterator<'js> { + fn into_js(self, _ctx: &Ctx<'js>) -> Result> { + Ok(self.into_value()) + } +} + +impl<'js> Object<'js> { + /// Returns whether the object is an iterator. + pub fn is_iterator(&self) -> bool { + self.get::<_, Function>("next").is_ok() + } + + /// Interpret as [`Iterator`] + /// + /// # Safety + /// You should be sure that the object actually is the required type. + pub unsafe fn ref_iterator(&self) -> &Iterator<'js> { + &*(self as *const _ as *const Iterator) + } + + /// Try reinterpret as [`Iterator`] + pub fn as_iterator(&self) -> Option<&Iterator<'js>> { + self.is_iterator().then_some(unsafe { self.ref_iterator() }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::*; + + #[test] + fn js_iterator_from_rust() { + test_with(|ctx| { + let iterator: Iterator = ctx + .eval( + r#" + const array = ['a', 'b', 'c']; + const iterator = array[Symbol.iterator](); + iterator + "#, + ) + .unwrap(); + assert_eq!( + iterator + .next() + .unwrap() + .unwrap() + .as_string() + .unwrap() + .to_string() + .unwrap(), + "a" + ); + assert_eq!( + iterator + .next() + .unwrap() + .unwrap() + .as_string() + .unwrap() + .to_string() + .unwrap(), + "b" + ); + assert_eq!( + iterator + .next() + .unwrap() + .unwrap() + .as_string() + .unwrap() + .to_string() + .unwrap(), + "c" + ); + assert!(iterator.next().unwrap().is_none()); + }); + } + + #[test] + fn rust_iterator_from_js() { + test_with(|ctx| { + let iterator = Iterator::new( + ctx.clone(), + IterFn::from(|_, position| { + if position < 3 { + Ok(Some(position)) + } else { + Ok(None) + } + }), + ) + .unwrap(); + ctx.globals().set("myiterator", iterator).unwrap(); + let res: String = ctx + .eval( + r#" + const res = []; + for (let i of myiterator) { + res.push(i); + } + res.join(',') + "#, + ) + .unwrap(); + assert_eq!(res.to_string().unwrap(), "0,1,2"); + }); + } + + #[test] + fn rust_iterator_from_rust() { + test_with(|ctx| { + let iterator = Iterator::new( + ctx.clone(), + IterFn::from(|_, position| { + if position < 3 { + Ok(Some(position)) + } else { + Ok(None) + } + }), + ) + .unwrap(); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 0); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 1); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 2); + assert!(iterator.next().unwrap().is_none()); + }); + } +} diff --git a/core/src/value/iterator/into_iter.rs b/core/src/value/iterator/into_iter.rs new file mode 100644 index 00000000..8e61c572 --- /dev/null +++ b/core/src/value/iterator/into_iter.rs @@ -0,0 +1,64 @@ +use std::iter::Iterator as StdIterator; + +use super::IntoJsIter; +use crate::{Ctx, IntoJs, Result}; + +impl<'js, T, I> IntoJsIter<'js, T::Item> for T +where + T: StdIterator, + I: IntoJs<'js>, +{ + fn next(&mut self, _ctx: Ctx<'_>, _position: usize) -> Result> { + Ok(self.next()) + } +} + +/// Helper type for creating an iterator from a closure which implements [`Fn`] +pub struct IterFn(F); + +impl IterFn { + pub fn new(f: F) -> Self { + IterFn(f) + } +} + +impl From for IterFn { + fn from(value: F) -> Self { + IterFn::new(value) + } +} + +impl<'js, F, I> IntoJsIter<'js, I> for IterFn +where + F: Fn(Ctx<'js>, usize) -> Result>, + I: IntoJs<'js>, +{ + fn next(&mut self, ctx: Ctx<'js>, position: usize) -> Result> { + self.0(ctx, position) + } +} + +/// Helper type for creating an iterator from a closure which implements [`FnMut`] +pub struct IterFnMut(F); + +impl IterFnMut { + pub fn new(f: F) -> Self { + IterFnMut(f) + } +} + +impl From for IterFnMut { + fn from(value: F) -> Self { + IterFnMut::new(value) + } +} + +impl<'js, F, I> IntoJsIter<'js, I> for IterFnMut +where + F: FnMut(Ctx<'js>, usize) -> Result>, + I: IntoJs<'js>, +{ + fn next(&mut self, ctx: Ctx<'js>, position: usize) -> Result> { + self.0(ctx, position) + } +} diff --git a/core/src/value/iterator/iterable.rs b/core/src/value/iterator/iterable.rs new file mode 100644 index 00000000..ea23485b --- /dev/null +++ b/core/src/value/iterator/iterable.rs @@ -0,0 +1,153 @@ +use std::ops::Deref; + +use crate::{ + atom::PredefinedAtom, + function::{IntoArgs, This}, + Ctx, Error, FromJs, Function, IntoJs, Iterator, Object, Result, Value, +}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct Iterable<'js>(pub(crate) Object<'js>); + +impl<'js> Iterable<'js> { + /// Get the iterator from the iterable. + /// + /// This is equivalent to calling `iterable[Symbol.iterator]()` in JavaScript. + pub fn iterator(&self) -> Result> { + let iter_fn = self.0.get::<_, Function>(PredefinedAtom::SymbolIterator)?; + let iterable = (This(self.0.clone()), 2).apply::>(&iter_fn)?; + Ok(Iterator(iterable)) + } + + /// Reference to value + #[inline] + pub fn as_value(&self) -> &Value<'js> { + self.0.as_value() + } + + /// Convert into value + #[inline] + pub fn into_value(self) -> Value<'js> { + self.0.into_value() + } + + /// Convert from value + pub fn from_value(value: Value<'js>) -> Option { + Self::from_object(Object::from_value(value).ok()?) + } + + /// Reference as an object + #[inline] + pub fn as_object(&self) -> &Object<'js> { + &self.0 + } + + /// Convert into an object + #[inline] + pub fn into_object(self) -> Object<'js> { + self.0 + } + + /// Convert from an object + pub fn from_object(object: Object<'js>) -> Option { + if object.is_iterable() { + Some(Self(object)) + } else { + None + } + } +} + +impl<'js> Deref for Iterable<'js> { + type Target = Object<'js>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'js> AsRef> for Iterable<'js> { + fn as_ref(&self) -> &Object<'js> { + &self.0 + } +} + +impl<'js> AsRef> for Iterable<'js> { + fn as_ref(&self) -> &Value<'js> { + self.0.as_ref() + } +} + +impl<'js> From> for Value<'js> { + fn from(value: Iterable<'js>) -> Self { + value.into_value() + } +} + +impl<'js> FromJs<'js> for Iterable<'js> { + fn from_js(_: &Ctx<'js>, value: Value<'js>) -> Result { + let ty_name = value.type_name(); + if let Some(v) = Self::from_value(value) { + Ok(v) + } else { + Err(Error::new_from_js(ty_name, "Iterable")) + } + } +} + +impl<'js> IntoJs<'js> for Iterable<'js> { + fn into_js(self, _ctx: &Ctx<'js>) -> Result> { + Ok(self.into_value()) + } +} + +impl<'js> Object<'js> { + /// Returns whether the object is iterable. + pub fn is_iterable(&self) -> bool { + self.contains_key(PredefinedAtom::SymbolIterator) + .unwrap_or(false) + } + + /// Interpret as [`Iterable`] + /// + /// # Safety + /// You should be sure that the object actually is the required type. + pub unsafe fn ref_iterable(&self) -> &Iterable<'js> { + &*(self as *const _ as *const Iterable) + } + + /// Try reinterpret as [`Iterable`] + pub fn as_iterable(&self) -> Option<&Iterable<'js>> { + self.is_iterable().then_some(unsafe { self.ref_iterable() }) + } +} + +#[cfg(test)] +mod test { + use crate::*; + + #[test] + fn from_javascript() { + test_with(|ctx| { + let iterable: Iterable = ctx + .eval( + r#" + const myIterable = {}; + myIterable[Symbol.iterator] = function* () { + yield 1; + yield 2; + yield 3; + }; + myIterable + "#, + ) + .unwrap(); + let iterator = iterable.iterator().unwrap(); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 1); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 2); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 3); + assert!(iterator.next().unwrap().is_none()); + }); + } +} From 000a2f17d1ef21ca1ae3985340617ff5acf408e7 Mon Sep 17 00:00:00 2001 From: Emile Fugulin Date: Sun, 26 May 2024 22:39:08 -0400 Subject: [PATCH 2/5] Add test for std iterator --- core/src/value/iterator.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/src/value/iterator.rs b/core/src/value/iterator.rs index 1388cfff..00dfa713 100644 --- a/core/src/value/iterator.rs +++ b/core/src/value/iterator.rs @@ -281,4 +281,25 @@ mod tests { assert!(iterator.next().unwrap().is_none()); }); } + + #[test] + fn rust_iterator_trait() { + test_with(|ctx| { + let data = vec![1, 2, 3]; + let iterator = Iterator::new(ctx.clone(), data.into_iter()).unwrap(); + ctx.globals().set("myiterator", iterator).unwrap(); + let res: String = ctx + .eval( + r#" + const res = []; + for (let i of myiterator) { + res.push(i); + } + res.join(',') + "#, + ) + .unwrap(); + assert_eq!(res.to_string().unwrap(), "1,2,3"); + }); + } } From 96f7f4b54a213a24844a17c92b9a033604d6f50d Mon Sep 17 00:00:00 2001 From: Emile Fugulin Date: Sun, 26 May 2024 22:58:09 -0400 Subject: [PATCH 3/5] Add conversion from JS iterator to rust --- core/src/value/iterator.rs | 54 +++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/core/src/value/iterator.rs b/core/src/value/iterator.rs index 00dfa713..e98d044c 100644 --- a/core/src/value/iterator.rs +++ b/core/src/value/iterator.rs @@ -1,4 +1,4 @@ -use std::ops::Deref; +use std::{iter::Iterator as StdIterator, marker::PhantomData, ops::Deref}; use crate::{ atom::PredefinedAtom, @@ -177,6 +177,36 @@ impl<'js> Object<'js> { } } +/// A rust iterator over the values of a js iterator. +pub struct IteratorIter<'js, T> { + iterator: Iterator<'js>, + marker: PhantomData, +} + +impl<'js, T> StdIterator for IteratorIter<'js, T> +where + T: FromJs<'js>, +{ + type Item = Result; + + fn next(&mut self) -> Option { + let next = self.iterator.next().transpose()?.ok()?; + Some(T::from_js(self.iterator.ctx(), next)) + } +} + +impl<'js> IntoIterator for Iterator<'js> { + type Item = Result>; + type IntoIter = IteratorIter<'js, Value<'js>>; + + fn into_iter(self) -> Self::IntoIter { + IteratorIter { + iterator: self, + marker: PhantomData, + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -231,6 +261,28 @@ mod tests { }); } + #[test] + fn js_iterator_from_rust_iter() { + test_with(|ctx| { + let values = ctx + .eval::( + r#" + const array = ['a', 'b', 'c']; + const iterator = array[Symbol.iterator](); + iterator + "#, + ) + .unwrap() + .into_iter() + .collect::>>() + .unwrap(); + assert_eq!(values.len(), 3); + assert_eq!(values[0].as_string().unwrap().to_string().unwrap(), "a"); + assert_eq!(values[1].as_string().unwrap().to_string().unwrap(), "b"); + assert_eq!(values[2].as_string().unwrap().to_string().unwrap(), "c"); + }); + } + #[test] fn rust_iterator_from_js() { test_with(|ctx| { From 412b6c0813b9e01e8558fab00224ecd9c0208ed3 Mon Sep 17 00:00:00 2001 From: Emile Fugulin Date: Sun, 26 May 2024 23:11:28 -0400 Subject: [PATCH 4/5] Add test for rust manual iterator --- core/src/value/iterator/iterable.rs | 48 ++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/core/src/value/iterator/iterable.rs b/core/src/value/iterator/iterable.rs index ea23485b..b210df75 100644 --- a/core/src/value/iterator/iterable.rs +++ b/core/src/value/iterator/iterable.rs @@ -125,7 +125,11 @@ impl<'js> Object<'js> { #[cfg(test)] mod test { - use crate::*; + use crate::{ + atom::PredefinedAtom, + function::{Func, This}, + *, + }; #[test] fn from_javascript() { @@ -150,4 +154,46 @@ mod test { assert!(iterator.next().unwrap().is_none()); }); } + + #[test] + fn from_rust() { + fn closure<'js>(ctx: Ctx<'js>) { + let myiterable = Object::new(ctx.clone()).unwrap(); + myiterable.set("position", 0usize).unwrap(); + myiterable + .set( + PredefinedAtom::SymbolIterator, + Func::from(|it: This>| -> Result> { Ok(it.0) }), + ) + .unwrap(); + myiterable + .set( + PredefinedAtom::Next, + Func::from( + move |ctx: Ctx<'js>, this: This>| -> Result> { + let position = this.get::<_, usize>("position")?; + let res = Object::new(ctx.clone())?; + if position >= 3 { + res.set(PredefinedAtom::Done, true)?; + } else { + res.set("value", position)?; + this.set("position", position + 1)?; + } + Ok(res) + }, + ), + ) + .unwrap(); + let iterator = Iterable::from_object(myiterable) + .unwrap() + .iterator() + .unwrap(); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 0); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 1); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 2); + assert!(iterator.next().unwrap().is_none()); + } + + test_with(closure); + } } From 8b80197df8b2d25b0f9e23a2237925497a13b74a Mon Sep 17 00:00:00 2001 From: Emile Fugulin Date: Sun, 26 May 2024 23:20:57 -0400 Subject: [PATCH 5/5] Use an associated type instead of generic --- core/src/value/iterator.rs | 11 +++++------ core/src/value/iterator/into_iter.rs | 12 +++++++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/core/src/value/iterator.rs b/core/src/value/iterator.rs index e98d044c..642c4391 100644 --- a/core/src/value/iterator.rs +++ b/core/src/value/iterator.rs @@ -13,11 +13,10 @@ pub use into_iter::{IterFn, IterFnMut}; pub use iterable::Iterable; /// A trait for converting a Rust object into a JavaScript iterator. -pub trait IntoJsIter<'js, I> -where - I: IntoJs<'js>, -{ - fn next(&mut self, ctx: Ctx<'js>, position: usize) -> Result>; +pub trait IntoJsIter<'js> { + type Item: IntoJs<'js>; + + fn next(&mut self, ctx: Ctx<'js>, position: usize) -> Result>; } /// A javascript iterator. @@ -30,7 +29,7 @@ impl<'js> Iterator<'js> { /// pub fn new(ctx: Ctx<'js>, mut it: T) -> Result where - T: IntoJsIter<'js, I> + 'js, + T: IntoJsIter<'js, Item = I> + 'js, I: IntoJs<'js>, { let iterator = Object::new(ctx.clone())?; diff --git a/core/src/value/iterator/into_iter.rs b/core/src/value/iterator/into_iter.rs index 8e61c572..f0e08305 100644 --- a/core/src/value/iterator/into_iter.rs +++ b/core/src/value/iterator/into_iter.rs @@ -3,11 +3,13 @@ use std::iter::Iterator as StdIterator; use super::IntoJsIter; use crate::{Ctx, IntoJs, Result}; -impl<'js, T, I> IntoJsIter<'js, T::Item> for T +impl<'js, T, I> IntoJsIter<'js> for T where T: StdIterator, I: IntoJs<'js>, { + type Item = I; + fn next(&mut self, _ctx: Ctx<'_>, _position: usize) -> Result> { Ok(self.next()) } @@ -28,11 +30,13 @@ impl From for IterFn { } } -impl<'js, F, I> IntoJsIter<'js, I> for IterFn +impl<'js, F, I> IntoJsIter<'js> for IterFn where F: Fn(Ctx<'js>, usize) -> Result>, I: IntoJs<'js>, { + type Item = I; + fn next(&mut self, ctx: Ctx<'js>, position: usize) -> Result> { self.0(ctx, position) } @@ -53,11 +57,13 @@ impl From for IterFnMut { } } -impl<'js, F, I> IntoJsIter<'js, I> for IterFnMut +impl<'js, F, I> IntoJsIter<'js> for IterFnMut where F: FnMut(Ctx<'js>, usize) -> Result>, I: IntoJs<'js>, { + type Item = I; + fn next(&mut self, ctx: Ctx<'js>, position: usize) -> Result> { self.0(ctx, position) }