diff --git a/RELEASES.md b/RELEASES.md index e1452780..96c0c55e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -11,6 +11,7 @@ Bug fixes --------- * Fixes compilation error when using the `internals` feature. Bug introduced in `0.19.4`. +* Importing script files recursively no longer panics. Breaking changes ---------------- @@ -22,6 +23,8 @@ Enhancements ------------ * Modules imported via `import` statements at global level can now be used in functions. There is no longer any need to re-`import` the modules at the beginning of each function block. +* `index_of`, `==` and `!=` are defined for arrays. +* `==` and `!=` are defined for object maps. Version 0.19.4 diff --git a/doc/src/language/arrays.md b/doc/src/language/arrays.md index 1b654385..492800b9 100644 --- a/doc/src/language/arrays.md +++ b/doc/src/language/arrays.md @@ -37,6 +37,9 @@ The following methods (mostly defined in the [`BasicArrayPackage`][packages] but | `+=` operator | 1) array
2) element to insert (not another array) | inserts an element at the end | | `+=` operator | 1) array
2) array to append | concatenates the second array to the end of the first | | `+` operator | 1) first array
2) second array | concatenates the first array with the second | +| `==` operator | 1) first array
2) second array | are the two arrays the same (elements compared with the `==` operator, if defined)? | +| `!=` operator | 1) first array
2) second array | are the two arrays different (elements compared with the `==` operator, if defined)? | +| `in` operator | item to find | does the array contain the item (compared with the `==` operator, if defined)? | | `insert` | 1) element to insert
2) position, beginning if < 0, end if > length | inserts an element at a certain index | | `pop` | _none_ | removes the last element and returns it ([`()`] if empty) | | `shift` | _none_ | removes the first element and returns it ([`()`] if empty) | @@ -54,6 +57,7 @@ The following methods (mostly defined in the [`BasicArrayPackage`][packages] but | `retain` | 1) start position, beginning if < 0, end if > length
2) number of items to retain, none if < 0 | retains a portion of the array, removes all other items and returning them (not in original order) | | `splice` | 1) start position, beginning if < 0, end if > length
2) number of items to remove, none if < 0
3) array to insert | replaces a portion of the array with another (not necessarily of the same length as the replaced portion) | | `filter` | [function pointer] to predicate (usually a [closure]) | constructs a new array with all items that return `true` when called with the predicate function:
1st parameter: array item
2nd parameter: _(optional)_ offset index | +| `index_of` | [function pointer] to predicate (usually a [closure]) | returns the index of the first item in the array that returns `true` when called with the predicate function, or -1 if not found:
1st parameter: array item
2nd parameter: _(optional)_ offset index | | `map` | [function pointer] to conversion function (usually a [closure]) | constructs a new array with all items mapped to the result of applying the conversion function:
1st parameter: array item
2nd parameter: _(optional)_ offset index | | `reduce` | 1) [function pointer] to accumulator function (usually a [closure])
2) _(optional)_ [function pointer] to function (usually a [closure]) that provides the initial value | reduces the array into a single value via the accumulator function:
1st parameter: accumulated value ([`()`] initially)
2nd parameter: array item
3rd parameter: _(optional)_ offset index | | `reduce_rev` | 1) [function pointer] to accumulator function (usually a [closure])
2) _(optional)_ [function pointer] to function (usually a [closure]) that provides the initial value | reduces the array (in reverse order) into a single value via the accumulator function:
1st parameter: accumulated value ([`()`] initially)
2nd parameter: array item
3rd parameter: _(optional)_ offset index | diff --git a/doc/src/language/fn-ptr.md b/doc/src/language/fn-ptr.md index 40fd8bcd..11ca1f4d 100644 --- a/doc/src/language/fn-ptr.md +++ b/doc/src/language/fn-ptr.md @@ -236,7 +236,7 @@ This type is normally provided by the [`Engine`] (e.g. when using [`Engine::regi However, it may also be manually constructed from a tuple: ```rust -use rhai::{Engine, FnPtr}; +use rhai::{Engine, FnPtr, NativeCallContext}; let engine = Engine::new(); @@ -254,13 +254,11 @@ let fn_ptr = engine.eval_ast::(&ast)?; // Get rid of the script, retaining only functions ast.retain_functions(|_, _, _| true); -// Create native call context via a tuple -let context = - ( - &engine, // the 'Engine' - &[ast.as_ref()] // function namespace from the 'AST' - // as a one-element slice - ).into(); +// Create native call context +let context = NativeCallContext::new( + &engine, // the 'Engine' + &[ast.as_ref()] // function namespace from the 'AST' +); // 'f' captures: the engine, the AST, and the closure let f = move |x: i64| fn_ptr.call_dynamic(context, None, [x.into()]); diff --git a/doc/src/language/object-maps.md b/doc/src/language/object-maps.md index 7c7fba24..3d24f7f5 100644 --- a/doc/src/language/object-maps.md +++ b/doc/src/language/object-maps.md @@ -59,12 +59,14 @@ operate on object maps: | Function | Parameter(s) | Description | | ---------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -| `has` | property name | does the object map contain a property of a particular name? | +| `has`, `in` operator | property name | does the object map contain a property of a particular name? | | `len` | _none_ | returns the number of properties | | `clear` | _none_ | empties the object map | | `remove` | property name | removes a certain property and returns it ([`()`] if the property does not exist) | | `+=` operator, `mixin` | second object map | mixes in all the properties of the second object map to the first (values of properties with the same names replace the existing values) | | `+` operator | 1) first object map
2) second object map | merges the first object map with the second | +| `==` operator | 1) first object map
2) second object map | are the two object map the same (elements compared with the `==` operator, if defined)? | +| `!=` operator | 1) first object map
2) second object map | are the two object map different (elements compared with the `==` operator, if defined)? | | `fill_with` | second object map | adds in all properties of the second object map that do not exist in the object map | | `keys` | _none_ | returns an [array] of all the property names (in random order), not available under [`no_index`] | | `values` | _none_ | returns an [array] of all the property values (in random order), not available under [`no_index`] | diff --git a/doc/src/start/examples/scripts.md b/doc/src/start/examples/scripts.md index d748d8a0..f3167a93 100644 --- a/doc/src/start/examples/scripts.md +++ b/doc/src/start/examples/scripts.md @@ -20,6 +20,7 @@ There are also a number of examples scripts that showcase Rhai's features, all i | [`function_decl3.rhai`]({{repoTree}}/scripts/function_decl3.rhai) | a [function] with many parameters | | [`if1.rhai`]({{repoTree}}/scripts/if1.rhai) | [`if`]({{rootUrl}}/language/if.md) example | | [`loop.rhai`]({{repoTree}}/scripts/loop.rhai) | count-down [`loop`]({{rootUrl}}/language/loop.md) in Rhai, emulating a `do` .. `while` loop | +| [`module.rhai`]({{repoTree}}/scripts/module.rhai) | import a script file as a module | | [`oop.rhai`]({{repoTree}}/scripts/oop.rhai) | simulate [object-oriented programming (OOP)][OOP] with [closures] | | [`op1.rhai`]({{repoTree}}/scripts/op1.rhai) | just simple addition | | [`op2.rhai`]({{repoTree}}/scripts/op2.rhai) | simple addition and multiplication | diff --git a/scripts/module.rhai b/scripts/module.rhai new file mode 100644 index 00000000..0ad387c1 --- /dev/null +++ b/scripts/module.rhai @@ -0,0 +1,3 @@ +import "scripts/loop"; + +print("Module test!"); diff --git a/src/engine.rs b/src/engine.rs index 6e47be93..a7f712e6 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -164,6 +164,7 @@ pub const FN_IDX_GET: &str = "index$get$"; pub const FN_IDX_SET: &str = "index$set$"; #[cfg(not(feature = "no_function"))] pub const FN_ANONYMOUS: &str = "anon$"; +pub const OP_EQUALS: &str = "=="; pub const MARKER_EXPR: &str = "$expr$"; pub const MARKER_BLOCK: &str = "$block$"; pub const MARKER_IDENT: &str = "$ident$"; @@ -1474,8 +1475,6 @@ impl Engine { match rhs_value { #[cfg(not(feature = "no_index"))] Dynamic(Union::Array(mut rhs_value)) => { - const OP_FUNC: &str = "=="; - // Call the `==` operator to compare each value let def_value = Some(false.into()); @@ -1485,11 +1484,11 @@ impl Engine { // Qualifiers (none) + function name + number of arguments + argument `TypeId`'s. let hash = - calc_native_fn_hash(empty(), OP_FUNC, args.iter().map(|a| a.type_id())); + calc_native_fn_hash(empty(), OP_EQUALS, args.iter().map(|a| a.type_id())); if self .call_native_fn( - mods, state, lib, OP_FUNC, hash, args, false, false, def_value, + mods, state, lib, OP_EQUALS, hash, args, false, false, def_value, ) .map_err(|err| err.fill_position(rhs.position()))? .0 diff --git a/src/engine_api.rs b/src/engine_api.rs index 6636481f..084c2040 100644 --- a/src/engine_api.rs +++ b/src/engine_api.rs @@ -1519,7 +1519,8 @@ impl Engine { let mut arg_values = args.into_vec(); let mut args: StaticVec<_> = arg_values.as_mut().iter_mut().collect(); - let result = self.call_fn_dynamic_raw(scope, ast.lib(), name, &mut None, args.as_mut())?; + let result = + self.call_fn_dynamic_raw(scope, &[ast.lib()], name, &mut None, args.as_mut())?; let typ = self.map_type_name(result.type_name()); @@ -1594,7 +1595,7 @@ impl Engine { ) -> Result> { let mut args: StaticVec<_> = arg_values.as_mut().iter_mut().collect(); - self.call_fn_dynamic_raw(scope, lib.as_ref(), name, &mut this_ptr, args.as_mut()) + self.call_fn_dynamic_raw(scope, &[lib.as_ref()], name, &mut this_ptr, args.as_mut()) } /// Call a script function defined in an `AST` with multiple `Dynamic` arguments. @@ -1610,13 +1611,14 @@ impl Engine { pub(crate) fn call_fn_dynamic_raw( &self, scope: &mut Scope, - lib: &Module, + lib: &[&Module], name: &str, this_ptr: &mut Option<&mut Dynamic>, args: &mut FnCallArgs, ) -> Result> { let fn_def = lib - .get_script_fn(name, args.len(), true) + .iter() + .find_map(|&m| m.get_script_fn(name, args.len(), true)) .ok_or_else(|| EvalAltResult::ErrorFunctionNotFound(name.into(), NO_POS))?; let mut state = Default::default(); @@ -1627,16 +1629,7 @@ impl Engine { ensure_no_data_race(name, args, false)?; } - self.call_script_fn( - scope, - &mut mods, - &mut state, - &[lib], - this_ptr, - fn_def, - args, - 0, - ) + self.call_script_fn(scope, &mut mods, &mut state, lib, this_ptr, fn_def, args, 0) } /// Optimize the `AST` with constants defined in an external Scope. diff --git a/src/fn_call.rs b/src/fn_call.rs index 5df04795..9911f2f2 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -571,7 +571,7 @@ impl Engine { _level, )? } else { - // Normal call of script function - map first argument to `this` + // Normal call of script function // The first argument is a reference? let mut backup: ArgBackup = Default::default(); backup.change_first_arg_to_copy(is_ref, args); diff --git a/src/fn_native.rs b/src/fn_native.rs index ef66eff3..cb6280c4 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -81,6 +81,31 @@ impl<'e, 'm, 'pm: 'm, M: AsRef<[&'pm Module]> + ?Sized> From<(&'e Engine, &'m M) } impl<'e, 'a, 'm, 'pm> NativeCallContext<'e, 'a, 'm, 'pm> { + /// Create a new `NativeCallContext`. + #[inline(always)] + pub fn new(engine: &'e Engine, lib: &'m impl AsRef<[&'pm Module]>) -> Self { + Self { + engine, + mods: None, + lib: lib.as_ref(), + } + } + /// _[INTERNALS]_ Create a new `NativeCallContext`. + /// Available under the `internals` feature only. + #[cfg(feature = "internals")] + #[cfg(not(feature = "no_module"))] + #[inline(always)] + pub fn new_with_imports( + engine: &'e Engine, + mods: &'a mut Imports, + lib: &'m impl AsRef<[&'pm Module]>, + ) -> Self { + Self { + engine, + mods: Some(mods), + lib: lib.as_ref(), + } + } /// The current `Engine`. #[inline(always)] pub fn engine(&self) -> &'e Engine { @@ -99,6 +124,54 @@ impl<'e, 'a, 'm, 'pm> NativeCallContext<'e, 'a, 'm, 'pm> { pub fn iter_namespaces(&self) -> impl Iterator + 'm { self.lib.iter().cloned() } + /// Call a function inside the call context. + /// + /// ## WARNING + /// + /// All arguments may be _consumed_, meaning that they may be replaced by `()`. + /// This is to avoid unnecessarily cloning the arguments. + /// Do not use the arguments after this call. If they are needed afterwards, + /// clone them _before_ calling this function. + /// + /// If `is_method` is `true`, the first argument is assumed to be passed + /// by reference and is not consumed. + pub fn call_fn_dynamic_raw( + &mut self, + fn_name: &str, + is_method: bool, + public_only: bool, + args: &mut [&mut Dynamic], + def_value: Option, + ) -> Result> { + let mut mods = self.mods.cloned().unwrap_or_default(); + + let hash_script = calc_script_fn_hash( + empty(), + fn_name, + if is_method { + args.len() - 1 + } else { + args.len() + }, + ); + + self.engine() + .exec_fn_call( + &mut mods, + &mut Default::default(), + self.lib, + fn_name, + hash_script, + args, + is_method, + is_method, + public_only, + None, + def_value, + 0, + ) + .map(|(r, _)| r) + } } /// Consume a `Shared` resource and return a mutable reference to the wrapped value. @@ -181,12 +254,11 @@ impl FnPtr { /// clone them _before_ calling this function. pub fn call_dynamic( &self, - ctx: NativeCallContext, + mut ctx: NativeCallContext, this_ptr: Option<&mut Dynamic>, mut arg_values: impl AsMut<[Dynamic]>, ) -> Result> { let arg_values = arg_values.as_mut(); - let fn_name = self.fn_name(); let mut args_data = self .curry() @@ -195,32 +267,15 @@ impl FnPtr { .chain(arg_values.iter_mut().map(mem::take)) .collect::>(); - let has_this = this_ptr.is_some(); let mut args = args_data.iter_mut().collect::>(); - let hash_script = calc_script_fn_hash(empty(), fn_name, args.len()); + + let has_this = this_ptr.is_some(); if let Some(obj) = this_ptr { args.insert(0, obj); } - let mut mods = ctx.mods.cloned().unwrap_or_default(); - - ctx.engine() - .exec_fn_call( - &mut mods, - &mut Default::default(), - ctx.lib, - fn_name, - hash_script, - args.as_mut(), - has_this, - has_this, - true, - None, - None, - 0, - ) - .map(|(v, _)| v) + ctx.call_fn_dynamic_raw(self.fn_name(), has_this, true, args.as_mut(), None) } } diff --git a/src/module/resolvers/file.rs b/src/module/resolvers/file.rs index 77756247..a76f26d0 100644 --- a/src/module/resolvers/file.rs +++ b/src/module/resolvers/file.rs @@ -4,7 +4,9 @@ use crate::module::{Module, ModuleResolver}; use crate::result::EvalAltResult; use crate::token::Position; -use crate::stdlib::{boxed::Box, collections::HashMap, path::PathBuf, string::String}; +use crate::stdlib::{ + boxed::Box, collections::HashMap, io::Error as IoError, path::PathBuf, string::String, +}; /// Module resolution service that loads module script files from the file system. /// @@ -135,34 +137,42 @@ impl ModuleResolver for FileModuleResolver { let scope = Default::default(); // See if it is cached - let mut module = None; + let mut module: Option> = None; - let module_ref = { + let mut module_ref = { #[cfg(not(feature = "sync"))] let c = self.cache.borrow(); #[cfg(feature = "sync")] let c = self.cache.read().unwrap(); if let Some(module) = c.get(&file_path) { - module.clone() + Some(module.clone()) } else { - // Load the file and compile it if not found - let ast = engine.compile_file(file_path.clone()).map_err(|err| { - Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos)) - })?; - - let mut m = Module::eval_ast_as_new(scope, &ast, engine).map_err(|err| { - Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos)) - })?; - - m.build_index(); - - let m: Shared = m.into(); - module = Some(m.clone()); - m + None } }; + if module_ref.is_none() { + // Load the script file and compile it + let ast = engine + .compile_file(file_path.clone()) + .map_err(|err| match *err { + EvalAltResult::ErrorSystem(_, err) if err.is::() => { + Box::new(EvalAltResult::ErrorModuleNotFound(path.to_string(), pos)) + } + _ => Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos)), + })?; + + let mut m = Module::eval_ast_as_new(scope, &ast, engine).map_err(|err| { + Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos)) + })?; + + m.build_index(); + + module = Some(m.into()); + module_ref = module.clone(); + }; + if let Some(module) = module { // Put it into the cache #[cfg(not(feature = "sync"))] @@ -171,6 +181,6 @@ impl ModuleResolver for FileModuleResolver { self.cache.write().unwrap().insert(file_path, module); } - Ok(module_ref) + Ok(module_ref.unwrap()) } } diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index 89609714..63abbf48 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -3,7 +3,7 @@ use crate::def_package; use crate::dynamic::Dynamic; -use crate::engine::Array; +use crate::engine::{Array, OP_EQUALS}; use crate::fn_native::{FnPtr, NativeCallContext}; use crate::plugin::*; use crate::result::EvalAltResult; @@ -263,6 +263,39 @@ mod array_functions { Ok(array.into()) } #[rhai_fn(return_raw)] + pub fn index_of( + ctx: NativeCallContext, + list: &mut Array, + filter: FnPtr, + ) -> Result> { + for (i, item) in list.iter().enumerate() { + if filter + .call_dynamic(ctx, None, [item.clone()]) + .or_else(|err| match *err { + EvalAltResult::ErrorFunctionNotFound(fn_sig, _) + if fn_sig.starts_with(filter.fn_name()) => + { + filter.call_dynamic(ctx, None, [item.clone(), (i as INT).into()]) + } + _ => Err(err), + }) + .map_err(|err| { + Box::new(EvalAltResult::ErrorInFunctionCall( + "filter".to_string(), + err, + NO_POS, + )) + })? + .as_bool() + .unwrap_or(false) + { + return Ok((i as INT).into()); + } + } + + Ok((-1 as INT).into()) + } + #[rhai_fn(return_raw)] pub fn some( ctx: NativeCallContext, list: &mut Array, @@ -619,6 +652,41 @@ mod array_functions { drained } + #[rhai_fn(name = "==", return_raw)] + pub fn equals( + mut ctx: NativeCallContext, + arr1: &mut Array, + mut arr2: Array, + ) -> Result> { + if arr1.len() != arr2.len() { + return Ok(false.into()); + } + if arr1.is_empty() { + return Ok(true.into()); + } + + let def_value = Some(false.into()); + + for (a1, a2) in arr1.iter_mut().zip(arr2.iter_mut()) { + let equals = ctx + .call_fn_dynamic_raw(OP_EQUALS, true, false, &mut [a1, a2], def_value.clone()) + .map(|v| v.as_bool().unwrap_or(false))?; + + if !equals { + return Ok(false.into()); + } + } + + Ok(true.into()) + } + #[rhai_fn(name = "!=", return_raw)] + pub fn not_equals( + ctx: NativeCallContext, + arr1: &mut Array, + arr2: Array, + ) -> Result> { + equals(ctx, arr1, arr2).map(|r| (!r.as_bool().unwrap()).into()) + } } gen_array_functions!(basic => INT, bool, char, ImmutableString, FnPtr, Array, Unit); diff --git a/src/packages/map_basic.rs b/src/packages/map_basic.rs index 045e55c0..104fbf6b 100644 --- a/src/packages/map_basic.rs +++ b/src/packages/map_basic.rs @@ -2,7 +2,7 @@ use crate::def_package; use crate::dynamic::Dynamic; -use crate::engine::Map; +use crate::engine::{Map, OP_EQUALS}; use crate::plugin::*; use crate::utils::ImmutableString; use crate::INT; @@ -46,6 +46,45 @@ mod map_functions { map1.entry(key).or_insert(value); }); } + #[rhai_fn(name = "==", return_raw)] + pub fn equals( + mut ctx: NativeCallContext, + map1: &mut Map, + mut map2: Map, + ) -> Result> { + if map1.len() != map2.len() { + return Ok(false.into()); + } + if map1.is_empty() { + return Ok(true.into()); + } + + let def_value = Some(false.into()); + + for (m1, v1) in map1.iter_mut() { + if let Some(v2) = map2.get_mut(m1) { + let equals = ctx + .call_fn_dynamic_raw(OP_EQUALS, true, false, &mut [v1, v2], def_value.clone()) + .map(|v| v.as_bool().unwrap_or(false))?; + + if !equals { + return Ok(false.into()); + } + } else { + return Ok(false.into()); + } + } + + Ok(true.into()) + } + #[rhai_fn(name = "!=", return_raw)] + pub fn not_equals( + ctx: NativeCallContext, + map1: &mut Map, + map2: Map, + ) -> Result> { + equals(ctx, map1, map2).map(|r| (!r.as_bool().unwrap()).into()) + } #[cfg(not(feature = "no_index"))] pub mod indexing { diff --git a/tests/modules.rs b/tests/modules.rs index ab51f4e4..97496881 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -432,3 +432,16 @@ fn test_module_ast_namespace2() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_module_file() -> Result<(), Box> { + let engine = Engine::new(); + let ast = engine.compile( + r#" + import "scripts/module"; + print("top"); + "#, + )?; + Module::eval_ast_as_new(Default::default(), &ast, &engine)?; + Ok(()) +}