From f7728f778e7f045b05b57424736eabe5500369a5 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 6 Jun 2024 10:13:07 +0800 Subject: [PATCH 1/3] New test for BuildType with operators. --- src/api/register.rs | 3 +-- tests/build_type.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/api/register.rs b/src/api/register.rs index aabe26fdb..245b2eec2 100644 --- a/src/api/register.rs +++ b/src/api/register.rs @@ -68,11 +68,10 @@ impl Engine { const X: bool, R: Variant + Clone, const F: bool, - FUNC: RhaiNativeFunc + SendSync + 'static, >( &mut self, name: impl AsRef + Into, - func: FUNC, + func: impl RhaiNativeFunc + SendSync + 'static, ) -> &mut Self { FuncRegistration::new(name.into()).register_into_engine(self, func); diff --git a/tests/build_type.rs b/tests/build_type.rs index f7a3b74d6..afd551893 100644 --- a/tests/build_type.rs +++ b/tests/build_type.rs @@ -1,5 +1,6 @@ #![cfg(not(feature = "no_object"))] use rhai::{CustomType, Engine, EvalAltResult, Position, TypeBuilder, INT}; +use std::cmp::Ordering; #[test] fn test_build_type() { @@ -219,3 +220,31 @@ fn test_build_type_macro() { } ); } + +#[test] +fn test_build_type_operators() { + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] + struct XYZ(INT); + + impl CustomType for XYZ { + fn build(mut tb: TypeBuilder) { + tb.with_fn("new_xyz", XYZ) + .with_fn("<", |a: XYZ, b: XYZ| Self::cmp(&a, &b) == Ordering::Less) + .with_fn(">", |a: XYZ, b: XYZ| Self::cmp(&a, &b) == Ordering::Greater) + .with_fn("<=", |a: XYZ, b: XYZ| Self::cmp(&a, &b) != Ordering::Greater) + .with_fn(">=", |a: XYZ, b: XYZ| Self::cmp(&a, &b) != Ordering::Less) + .with_fn("!=", |a: XYZ, b: XYZ| Self::cmp(&a, &b) != Ordering::Equal) + .with_fn("==", |a: XYZ, b: XYZ| Self::cmp(&a, &b) == Ordering::Equal); + } + } + + let mut engine = Engine::new(); + engine.build_type::(); + + assert!(!engine.eval::("let a = new_xyz(1); let b = new_xyz(2); a == b").unwrap()); + assert!(engine.eval::("let a = new_xyz(1); let b = new_xyz(2); a != b").unwrap()); + assert!(engine.eval::("let a = new_xyz(1); let b = new_xyz(2); a < b").unwrap()); + assert!(engine.eval::("let a = new_xyz(1); let b = new_xyz(2); a <= b").unwrap()); + assert!(!engine.eval::("let a = new_xyz(1); let b = new_xyz(2); a > b").unwrap()); + assert!(!engine.eval::("let a = new_xyz(1); let b = new_xyz(2); a >= b").unwrap()); +} From 423f21edf98caf63b9d6cc18974977758b309ade Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 13 Jun 2024 11:35:11 +0800 Subject: [PATCH 2/3] Add filter, drain and retain to maps. --- CHANGELOG.md | 1 + src/packages/map_basic.rs | 160 +++++++++++++++++++++++++++++++++++++- 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a019d4dac..61fc507bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ New features * The `break`, `continue`, `return` and `throw` statements can now follow the `??` operator to short-circuit operations where the value is `()`. * A new symbol, `$func$`, is added to custom syntax to allow parsing of anonymous functions. +* The `filter`, `drain` and `retain` methods are added to object maps. Version 1.18.0 diff --git a/src/packages/map_basic.rs b/src/packages/map_basic.rs index 83c3e446d..bde27ac13 100644 --- a/src/packages/map_basic.rs +++ b/src/packages/map_basic.rs @@ -2,7 +2,9 @@ use crate::engine::OP_EQUALS; use crate::plugin::*; -use crate::{def_package, Dynamic, ImmutableString, Map, NativeCallContext, RhaiResultOf, INT}; +use crate::{ + def_package, Dynamic, FnPtr, ImmutableString, Map, NativeCallContext, RhaiResultOf, INT, +}; #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -293,6 +295,162 @@ mod map_functions { map.values().cloned().collect() } + /// Iterate through all the elements in the object map, applying a `filter` function to each + /// and return a new collection of all elements that return `true` as a new object map. + /// + /// # Function Parameters + /// + /// * `key`: current key + /// * `value` _(optional)_: copy of element (bound to `this` if omitted) + /// + /// # Example + /// + /// ```rhai + /// let x = #{a:1, b:2, c:3, d:4, e:5}; + /// + /// let y = x.filter(|k| this >= 3); + /// + /// print(y); // prints #{"c":3, "d":4, "e":5} + /// + /// let y = x.filter(|k, v| k != "d" && v < 5); + /// + /// print(y); // prints #{"a":1, "b":2, "c":3} + /// ``` + #[rhai_fn(return_raw)] + pub fn filter(ctx: NativeCallContext, map: &mut Map, filter: FnPtr) -> RhaiResultOf { + if map.is_empty() { + return Ok(Map::new()); + } + + let mut result = Map::new(); + + for (key, item) in map.iter_mut() { + if filter + .call_raw_with_extra_args("filter", &ctx, Some(item), [key.into()], [], Some(1))? + .as_bool() + .unwrap_or(false) + { + result.insert(key.clone(), item.clone()); + } + } + + Ok(result) + } + /// Remove all elements in the object map that return `true` when applied the `filter` function and + /// return them as a new object map. + /// + /// # Function Parameters + /// + /// * `key`: current key + /// * `value` _(optional)_: copy of element (bound to `this` if omitted) + /// + /// # Example + /// + /// ```rhai + /// let x = #{a:1, b:2, c:3, d:4, e:5}; + /// + /// let y = x.drain(|k| this < 3); + /// + /// print(x); // prints #{"c":3, "d":4, "e":5] + /// + /// print(y); // prints #{"a":1, "b"2} + /// + /// let z = x.drain(|k, v| k == "c" || v >= 5); + /// + /// print(x); // prints #{"d":4} + /// + /// print(z); // prints #{"c":3, "e":5} + /// ``` + #[rhai_fn(return_raw)] + pub fn drain(ctx: NativeCallContext, map: &mut Map, filter: FnPtr) -> RhaiResultOf { + if map.is_empty() { + return Ok(Map::new()); + } + + let mut drained = Map::new(); + let mut retained = Map::new(); + + for (key, mut value) in mem::take(map).into_iter() { + if filter + .call_raw_with_extra_args( + "drain", + &ctx, + Some(&mut value), + [key.clone().into()], + [], + Some(1), + )? + .as_bool() + .unwrap_or(false) + { + drained.insert(key, value); + } else { + retained.insert(key, value); + } + } + + *map = retained; + + Ok(drained) + } + /// Remove all elements in the object map that do not return `true` when applied the `filter` function and + /// return them as a new object map. + /// + /// # Function Parameters + /// + /// * `key`: current key + /// * `value` _(optional)_: copy of element (bound to `this` if omitted) + /// + /// # Example + /// + /// ```rhai + /// let x = #{a:1, b:2, c:3, d:4, e:5}; + /// + /// let y = x.retain(|k| this < 3); + /// + /// print(x); // prints #{"a":1, "b"2} + /// + /// print(y); // prints #{"c":3, "d":4, "e":5] + /// + /// let z = y.retain(|k, v| k == "c" || v >= 5); + /// + /// print(y); // prints #{"c":3, "e":5} + /// + /// print(z); // prints #{"d":4} + /// ``` + #[rhai_fn(return_raw)] + pub fn retain(ctx: NativeCallContext, map: &mut Map, filter: FnPtr) -> RhaiResultOf { + if map.is_empty() { + return Ok(Map::new()); + } + + let mut drained = Map::new(); + let mut retained = Map::new(); + + for (key, mut value) in mem::take(map).into_iter() { + if filter + .call_raw_with_extra_args( + "retain", + &ctx, + Some(&mut value), + [key.clone().into()], + [], + Some(1), + )? + .as_bool() + .unwrap_or(false) + { + retained.insert(key, value); + } else { + drained.insert(key, value); + } + } + + *map = retained; + + Ok(drained) + } + /// Return the JSON representation of the object map. /// /// # Data types From 112e6e4054dd93a4383d1887cda137d2e9c6dfa3 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 15 Jun 2024 13:07:07 +0800 Subject: [PATCH 3/3] Fix stack overflow --- CHANGELOG.md | 1 + src/types/dynamic.rs | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61fc507bd..637e185ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Bug fixes * Fixed stack overflow when calling closures recursively (thanks [`@MageWeiG`](https://github.com/MageWeiG) [880](https://github.com/rhaiscript/rhai/issues/880)). * `Engine::call_fn` and `Engine::call_fn_with_options` now correctly use the `AST`'s `source` field. * (Fuzzing) Fixed crash when using `..=` in strings. +* A recursive stack-overflow bug in `Dynamic::is_hashable` is fixed. New features ------------ diff --git a/src/types/dynamic.rs b/src/types/dynamic.rs index a6fe6e3db..cb3800e3c 100644 --- a/src/types/dynamic.rs +++ b/src/types/dynamic.rs @@ -741,7 +741,6 @@ impl fmt::Debug for Dynamic { dict: &mut HashSet<*const Dynamic>, ) -> fmt::Result { match value.0 { - #[cfg(not(feature = "no_closure"))] Union::Shared(ref cell, ..) => match crate::func::locked_read(cell) { Some(v) => { if dict.insert(value) { @@ -1267,8 +1266,27 @@ impl Dynamic { } #[cfg(not(feature = "no_closure"))] - Union::Shared(ref cell, ..) => { - crate::func::locked_read(cell).map_or(false, |v| v.is_hashable()) + Union::Shared(..) => { + #[cfg(feature = "no_std")] + use hashbrown::HashSet; + #[cfg(not(feature = "no_std"))] + use std::collections::HashSet; + + // Avoid infinite recursion for shared values in a reference loop. + fn checked_is_hashable( + value: &Dynamic, + dict: &mut HashSet<*const Dynamic>, + ) -> bool { + match value.0 { + Union::Shared(ref cell, ..) => match crate::func::locked_read(cell) { + Some(v) => dict.insert(value) && checked_is_hashable(&v, dict), + _ => false, + }, + _ => value.is_hashable(), + } + } + + checked_is_hashable(self, &mut <_>::default()) } } }