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(())
+}