diff --git a/RELEASES.md b/RELEASES.md index c7ce117c..73fd61d2 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,6 +4,8 @@ Rhai Release Notes Version 0.20.0 ============== +This version adds a variable resolver with the ability to short-circuit variable access. + Breaking changes ---------------- @@ -17,6 +19,8 @@ Breaking changes * `rhai::ser` and `rhai::de` namespaces are merged into `rhai::serde`. * New reserved symbols: `++`, `--`, `..`, `...`. * Callback signature for custom syntax implementation function is changed to allow for more flexibility. +* Default call stack depth for `debug` builds is reduced to 12 (from 16). +* Precedence for `~` and `%` is raised. New features ------------ @@ -24,15 +28,15 @@ New features * New `Engine::on_var` to register a _variable resolver_. * `const` statements can now take any expression (or none at all) instead of only constant values. * `OptimizationLevel::Simple` now eagerly evaluates built-in binary operators of primary types (if not overloaded). -* Added `is_def_var()` to detect if variable is defined, and `is_def_fn()` to detect if script function is defined. +* `is_def_var()` to detect if variable is defined, and `is_def_fn()` to detect if script function is defined. * `Dynamic::from(&str)` now constructs a `Dynamic` with a copy of the string as value. * `AST::combine` and `AST::combine_filtered` allows combining two `AST`'s without creating a new one. +* `map`, `filter`, `reduce`, `reduce_rev`, `some`, `all`, `splice` and `sort` functions for arrays. Enhancements ------------ * Many one-liners and few-liners are now marked `#[inline]` or `[inline(always)]`, just in case it helps when LTO is not turned on. -* Default call stack depth for `debug` builds is reduced to 12 (from 16). Version 0.19.0 diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 2cef773b..84a9e233 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -17,6 +17,8 @@ Easy * Very few additional dependencies - right now only [`smallvec`](https://crates.io/crates/smallvec/) plus crates for procedural macros; for [`no-std`] and `WASM` builds, a number of additional dependencies are pulled in to provide for missing functionalities. +* [Plugins] system powered by procedural macros simplifies custom API development. + Fast ---- @@ -41,6 +43,8 @@ Dynamic * Some support for [object-oriented programming (OOP)][OOP]. +* Hook into variables access via [variable resolver]. + Safe ---- @@ -66,9 +70,6 @@ Flexible * Supports [most build targets](targets.md) including `no-std` and [WASM]. -* [Plugins] system powered by procedural macros simplifies custom API development. - * Surgically [disable keywords and operators] to restrict the language. -* Use as a [DSL] by [disabling keywords/operators][disable keywords and operators], [custom operators] - and extending the language with [custom syntax]. +* Use as a [DSL] by defining [custom operators] and/or extending the language with [custom syntax]. diff --git a/doc/src/about/non-design.md b/doc/src/about/non-design.md index 720a7c12..897efbfa 100644 --- a/doc/src/about/non-design.md +++ b/doc/src/about/non-design.md @@ -10,7 +10,7 @@ It doesn't attempt to be a new language. For example: * No traits... so it is also not Rust. Do your Rusty stuff in Rust. -* No structures/records - define your types in Rust instead; Rhai can seamlessly work with _any Rust type_. +* No structures/records/tuples - define your types in Rust instead; Rhai can seamlessly work with _any Rust type_. There is, however, a built-in [object map] type which is adequate for most uses. It is possible to simulate [object-oriented programming (OOP)][OOP] by storing [function pointers] @@ -32,7 +32,7 @@ It doesn't attempt to be a new language. For example: integrate with native Rust applications. * No formal language grammar - Rhai uses a hand-coded lexer, a hand-coded top-down recursive-descent parser - for statements and a Pratt parser for expressions. + for statements, and a hand-coded Pratt parser for expressions. This lack of formalism allows the parser itself to be exposed as a service in order to support [disabling keywords/operators][disable keywords and operators], adding [custom operators], @@ -43,8 +43,8 @@ Do Not Write The Next 4D VR Game in Rhai --------------------------------------- Due to this intended usage, Rhai deliberately keeps the language simple and small by omitting -advanced language features such as classes, inheritance, first-class functions, closures, -concurrency, byte-codes VM, JIT etc. +advanced language features such as classes, inheritance, interfaces, generics, +first-class functions/closures, pattern matching, concurrency, byte-codes VM, JIT etc. Avoid the temptation to write full-fledge application logic entirely in Rhai - that use case is best fulfilled by more complete languages such as JavaScript or Lua. diff --git a/doc/src/engine/custom-op.md b/doc/src/engine/custom-op.md index 79f70d7c..2e98ecc1 100644 --- a/doc/src/engine/custom-op.md +++ b/doc/src/engine/custom-op.md @@ -95,7 +95,8 @@ The following _precedence table_ show the built-in precedence of standard Rhai o | Comparisons | `>`, `>=`, `<`, `<=` | 110 | | | `in` | 130 | | Arithmetic | `+`, `-` | 150 | -| Arithmetic | `*`, `/`, `~`, `%` | 180 | +| Arithmetic | `*`, `/` | 180 | +| Arithmetic | `~`, `%` | 190 | | Bit-shifts | `<<`, `>>` | 210 | | Object | `.` _(binds to right)_ | 240 | | _Others_ | | 0 | diff --git a/doc/src/engine/var.md b/doc/src/engine/var.md index bcd75069..a6cc0cf3 100644 --- a/doc/src/engine/var.md +++ b/doc/src/engine/var.md @@ -10,7 +10,7 @@ searches the [`Scope`] that is passed into the `Engine::eval` call. There is a built-in facility for advanced users to _hook_ into the variable resolution service and to override its default behavior. -To do so, provide a closure to the [`Engine`] via the [`Engine::on_var`] method: +To do so, provide a closure to the [`Engine`] via the `Engine::on_var` method: ```rust let mut engine = Engine::new(); @@ -45,6 +45,18 @@ a variable resolver. Then these variables can be assigned to and their updated v the script is evaluated. +Benefits of Using a Variable Resolver +------------------------------------ + +1. Avoid having to maintain a custom [`Scope`] with all variables regardless of need (because a script may not use them all). + +2. _Short-circuit_ variable access, essentially overriding standard behavior. + +3. _Lazy-load_ variables when they are accessed, not up-front. This benefits when the number of variables is very large, when they are timing-dependent, or when they are expensive to load. + +4. Rename system variables on a script-by-script basis without having to construct different [`Scope`]'s. + + Function Signature ------------------ @@ -61,12 +73,20 @@ where: If `index` is zero, then there is no pre-calculated offset position and a search through the current [`Scope`] must be performed. -* `scope : &Scope` - reference to the current [`Scope`] containing all variables up to the current evaluation position. +* `scope: &Scope` - reference to the current [`Scope`] containing all variables up to the current evaluation position. * `context: &EvalContext` - reference to the current evaluation _context_, which exposes the following fields: * `context.engine(): &Engine` - reference to the current [`Engine`]. * `context.namespace(): &Module` - reference to the current _global namespace_ (as a [module]) containing all script-defined functions. * `context.call_level(): usize` - the current nesting level of function calls. -The return value is `Result, Box>` where `Ok(None)` indicates that the normal -variable resolution process should continue. +### Return Value + +The return value is `Result, Box>` where: + +* `Ok(None)` - normal variable resolution process should continue, meaning to continue searching through the [`Scope`]. + +* `Ok(Some(Dynamic))` - wrapped [`Dynamic`] is taken as the value of the variable, which is treated as a constant. + +* `Err(Box)` - error is reflected back to the [`Engine`]. + Normally this is `EvalAltResult::ErrorVariableNotFound` to indicate that the variable does not exist, but it can be any error. diff --git a/doc/src/language/arrays.md b/doc/src/language/arrays.md index 29e1aafb..437364d6 100644 --- a/doc/src/language/arrays.md +++ b/doc/src/language/arrays.md @@ -30,22 +30,30 @@ Built-in Functions The following methods (mostly defined in the [`BasicArrayPackage`][packages] but excluded if using a [raw `Engine`]) operate on arrays: -| Function | Parameter(s) | Description | -| ------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| `push` | element to insert | inserts an element at the end | -| `append` | array to append | concatenates the second array to the end of the first | -| `+=` 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 | -| `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) | -| `remove` | index | removes an element at a particular index and returns it, or returns [`()`] if the index is not valid | -| `reverse` | _none_ | reverses the array | -| `len` method and property | _none_ | returns the number of elements | -| `pad` | 1) target length
2) element to pad | pads the array with an element to at least a specified length | -| `clear` | _none_ | empties the array | -| `truncate` | target length | cuts off the array at exactly a specified length (discarding all subsequent elements) | +| Function | Parameter(s) | Description | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `push` | element to insert | inserts an element at the end | +| `append` | array to append | concatenates the second array to the end of the first | +| `+=` 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 | +| `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) | +| `remove` | index | removes an element at a particular index and returns it ([`()`] if the index is not valid) | +| `reverse` | _none_ | reverses the array | +| `len` method and property | _none_ | returns the number of elements | +| `pad` | 1) target length
2) element to pad | pads the array with an element to at least a specified length | +| `clear` | _none_ | empties the array | +| `truncate` | target length | cuts off the array at exactly a specified length (discarding all subsequent elements) | +| `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 (can be a [closure]) | constructs a new array with all items that returns `true` when called with the predicate function:
1st parameter: array item,
2nd parameter: offset index (optional) | +| `map` | [function pointer] to conversion function (can be a [closure]) | constructs a new array with all items mapped to the result of applying the conversion function:
1st parameter: array item,
2nd parameter: offset index (optional) | +| `reduce` | [function pointer] to accumulator function (can be a [closure]) | constructs a new array with all items accumulated by the accumulator function:
1st parameter: accumulated value ([`()`] initially),
2nd parameter: array item,
3rd parameter: offset index (optional) | +| `reduce_rev` | [function pointer] to accumulator function (can be a [closure]) | constructs a new array with all items (in reverse order) accumulated by the accumulator function:
1st parameter: accumulated value ([`()`] initially),
2nd parameter: array item,
3rd parameter: offset index (optional) | +| `some` | [function pointer] to predicate (can be a [closure]) | returns `true` if any item returns `true` when called with the predicate function:
1st parameter: array item,
2nd parameter: offset index (optional) | +| `all` | [function pointer] to predicate (can be a [closure]) | returns `true` if all item returns `true` when called with the predicate function:
1st parameter: array item,
2nd parameter: offset index (optional) | +| `sort` | [function pointer] to a comparison function (can be a [closure]) | sorts the array with a comparison function:
1st parameter: first item,
2nd parameter: second item | Use Custom Types With Arrays @@ -63,12 +71,12 @@ Examples -------- ```rust -let y = [2, 3]; // array literal with 2 elements +let y = [2, 3]; // array literal with 2 elements -let y = [2, 3,]; // trailing comma is OK +let y = [2, 3,]; // trailing comma is OK -y.insert(0, 1); // insert element at the beginning -y.insert(999, 4); // insert element at the end +y.insert(0, 1); // insert element at the beginning +y.insert(999, 4); // insert element at the end y.len == 4; @@ -77,21 +85,21 @@ y[1] == 2; y[2] == 3; y[3] == 4; -(1 in y) == true; // use 'in' to test if an item exists in the array -(42 in y) == false; // 'in' uses the '==' operator (which users can override) - // to check if the target item exists in the array +(1 in y) == true; // use 'in' to test if an item exists in the array +(42 in y) == false; // 'in' uses the '==' operator (which users can override) + // to check if the target item exists in the array -y[1] = 42; // array elements can be reassigned +y[1] = 42; // array elements can be reassigned (42 in y) == true; -y.remove(2) == 3; // remove element +y.remove(2) == 3; // remove element y.len == 3; -y[2] == 4; // elements after the removed element are shifted +y[2] == 4; // elements after the removed element are shifted -ts.list = y; // arrays can be assigned completely (by value copy) +ts.list = y; // arrays can be assigned completely (by value copy) let foo = ts.list[1]; foo == 42; @@ -99,7 +107,7 @@ let foo = [1, 2, 3][0]; foo == 1; fn abc() { - [42, 43, 44] // a function returning an array + [42, 43, 44] // a function returning an array } let foo = abc()[0]; @@ -108,32 +116,76 @@ foo == 42; let foo = y[0]; foo == 1; -y.push(4); // 4 elements -y += 5; // 5 elements +y.push(4); // 4 elements +y += 5; // 5 elements y.len == 5; -let first = y.shift(); // remove the first element, 4 elements remaining +let first = y.shift(); // remove the first element, 4 elements remaining first == 1; -let last = y.pop(); // remove the last element, 3 elements remaining +let last = y.pop(); // remove the last element, 3 elements remaining last == 5; y.len == 3; -for item in y { // arrays can be iterated with a 'for' statement +for item in y { // arrays can be iterated with a 'for' statement print(item); } -y.pad(10, "hello"); // pad the array up to 10 elements +y.pad(10, "hello"); // pad the array up to 10 elements y.len == 10; -y.truncate(5); // truncate the array to 5 elements +y.truncate(5); // truncate the array to 5 elements y.len == 5; -y.clear(); // empty the array +y.clear(); // empty the array y.len == 0; + +let a = [42, 123, 99]; + +a.map(|v| v + 1); // [43, 124, 100] + +a.map(|v, i| v + i); // [42, 124, 101] + +a.filter(|v| v > 50); // [123, 99] + +a.filter(|v, i| i == 1); // [123] + +a.reduce(|sum, v| { + // Detect the initial value of '()' + if sum.type_of() == "()" { v } else { sum + v } +) == 264; + +a.reduce(|sum, v, i| { + if i == 0 { v } else { sum + v } +) == 264; + +a.reduce_rev(|sum, v| { + // Detect the initial value of '()' + if sum.type_of() == "()" { v } else { sum + v } +) == 264; + +a.reduce_rev(|sum, v, i| { + if i == 0 { v } else { sum + v } +) == 264; + +a.some(|v| v > 50) == true; + +a.some(|v, i| v < i) == false; + +a.all(|v| v > 50) == false; + +a.all(|v, i| v > i) == true; + +a.splice(1, 1, [1, 3, 2]); + +a == [42, 1, 3, 2, 99]; + +a.sort(|x, y| x - y); + +a == [1, 2, 3, 42, 99]; ``` diff --git a/doc/src/language/fn-capture.md b/doc/src/language/fn-capture.md index 58ce811a..e35de979 100644 --- a/doc/src/language/fn-capture.md +++ b/doc/src/language/fn-capture.md @@ -43,6 +43,12 @@ let f = Fn("foo"); call!(f, 41) == 42; // must use function-call style f.call!(41); // <- syntax error: capturing is not allowed in method-call style + +// Capturing is not available for module functions + +import "hello" as h; + +h::greet!(); // <- syntax error: capturing is not allowed in namespace-qualified calls ``` diff --git a/doc/src/patterns/dynamic-const.md b/doc/src/patterns/dynamic-const.md index b370e099..e3fe7c06 100644 --- a/doc/src/patterns/dynamic-const.md +++ b/doc/src/patterns/dynamic-const.md @@ -35,8 +35,8 @@ Implementation let mut engine = Engine::new(); // Create shared data provider. -// Assume that Provider::get(&str) -> Option gets a system constant. -let provider: Arc = get_data_provider(); +// Assume that SystemValuesProvider::get(&str) -> Option gets a value. +let provider = Arc::new(SystemValuesProvider::new()); // Clone the shared provider let db = provider.clone(); diff --git a/doc/src/rust/print-custom.md b/doc/src/rust/print-custom.md index 2ea6f1e8..028a3519 100644 --- a/doc/src/rust/print-custom.md +++ b/doc/src/rust/print-custom.md @@ -5,7 +5,7 @@ Printing for Custom Types To use custom types for [`print`] and [`debug`], or convert its value into a [string], it is necessary that the following functions be registered (assuming the custom type -is `T : Display + Debug`): +is `T: Display + Debug`): | Function | Signature | Typical implementation | Usage | | ----------- | ---------------------------------------------- | ---------------------------- | -------------------------------------------------------------------- | diff --git a/doc/src/rust/register-raw.md b/doc/src/rust/register-raw.md index 2897138f..2231320e 100644 --- a/doc/src/rust/register-raw.md +++ b/doc/src/rust/register-raw.md @@ -63,14 +63,14 @@ The function signature passed to `Engine::register_raw_fn` takes the following f where: -* `T : Variant + Clone` - return type of the function. +* `T: Variant + Clone` - return type of the function. -* `engine : &Engine` - the current [`Engine`], with all configurations and settings. +* `engine: &Engine` - the current [`Engine`], with all configurations and settings. -* `lib : &Module` - the current global library of script-defined functions, as a [`Module`]. +* `lib: &Module` - the current global library of script-defined functions, as a [`Module`]. This is sometimes useful for calling a script-defined function within the same evaluation context using [`Engine::call_fn`][`call_fn`]. -* `args : &mut [&mut Dynamic]` - a slice containing `&mut` references to [`Dynamic`] values. +* `args: &mut [&mut Dynamic]` - a slice containing `&mut` references to [`Dynamic`] values. The slice is guaranteed to contain enough arguments _of the correct types_. Remember, in Rhai, all arguments _except_ the _first_ one are always passed by _value_ (i.e. cloned). diff --git a/src/engine.rs b/src/engine.rs index 5c47a3cf..127a4277 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -37,6 +37,7 @@ use crate::stdlib::{ collections::{HashMap, HashSet}, fmt, format, iter::{empty, once}, + num::NonZeroUsize, ops::DerefMut, string::{String, ToString}, vec::Vec, @@ -250,8 +251,8 @@ impl<'a> Target<'a> { Self::LockGuard(_) => (), #[cfg(not(feature = "no_index"))] Self::StringChar(_, _, ch) => { - let new_val = ch.clone(); - self.set_value((new_val, Position::none()), Position::none()) + let char_value = ch.clone(); + self.set_value((char_value, Position::none()), Position::none()) .unwrap(); } } @@ -284,10 +285,9 @@ impl<'a> Target<'a> { })?; let mut chars = s.chars().collect::>(); - let ch = chars[*index]; // See if changed - if so, update the String - if ch != new_ch { + if chars[*index] != new_ch { chars[*index] = new_ch; *s = chars.iter().collect::().into(); } @@ -502,6 +502,13 @@ pub fn make_setter(id: &str) -> String { format!("{}{}", FN_SET, id) } +/// Is this function an anonymous function? +#[cfg(not(feature = "no_function"))] +#[inline(always)] +pub fn is_anonymous_fn(fn_name: &str) -> bool { + fn_name.starts_with(FN_ANONYMOUS) +} + /// Print/debug to stdout fn default_print(_s: &str) { #[cfg(not(feature = "no_std"))] @@ -520,13 +527,13 @@ pub fn search_imports<'s>( // Qualified - check if the root module is directly indexed let index = if state.always_search { - None + 0 } else { - modules.index() + modules.index().map_or(0, NonZeroUsize::get) }; - Ok(if let Some(index) = index { - let offset = mods.len() - index.get(); + Ok(if index > 0 { + let offset = mods.len() - index; &mods.get(offset).unwrap().1 } else { mods.iter() @@ -548,13 +555,13 @@ pub fn search_imports_mut<'s>( // Qualified - check if the root module is directly indexed let index = if state.always_search { - None + 0 } else { - modules.index() + modules.index().map_or(0, NonZeroUsize::get) }; - Ok(if let Some(index) = index { - let offset = mods.len() - index.get(); + Ok(if index > 0 { + let offset = mods.len() - index; &mut mods.get_mut(offset).unwrap().1 } else { mods.iter_mut() @@ -684,19 +691,15 @@ impl Engine { // Qualified variable ((name, pos), Some(modules), hash_var, _) => { let module = search_imports_mut(mods, state, modules)?; - let target = - module - .get_qualified_var_mut(*hash_var) - .map_err(|err| match *err { - EvalAltResult::ErrorVariableNotFound(_, _) => { - EvalAltResult::ErrorVariableNotFound( - format!("{}{}", modules, name), - *pos, - ) - .into() - } - _ => err.fill_position(*pos), - })?; + let target = module.get_qualified_var_mut(*hash_var).map_err(|mut err| { + match *err { + EvalAltResult::ErrorVariableNotFound(ref mut err_name, _) => { + *err_name = format!("{}{}", modules, name); + } + _ => (), + } + err.fill_position(*pos) + })?; // Module variables are constant Ok((target.into(), name, ScopeEntryType::Constant, *pos)) @@ -733,7 +736,11 @@ impl Engine { } // Check if it is directly indexed - let index = if state.always_search { None } else { *index }; + let index = if state.always_search { + 0 + } else { + index.map_or(0, NonZeroUsize::get) + }; // Check the variable resolver, if any if let Some(ref resolve_var) = self.resolve_var { @@ -745,15 +752,15 @@ impl Engine { this_ptr, level: 0, }; - if let Some(result) = resolve_var(name, index.map_or(0, |v| v.get()), scope, &context) - .map_err(|err| err.fill_position(*pos))? + if let Some(result) = + resolve_var(name, index, scope, &context).map_err(|err| err.fill_position(*pos))? { return Ok((result.into(), name, ScopeEntryType::Constant, *pos)); } } - let index = if let Some(index) = index { - scope.len() - index.get() + let index = if index > 0 { + scope.len() - index } else { // Find the variable in the scope scope @@ -1604,10 +1611,10 @@ impl Engine { // Module-qualified function call Expr::FnCall(x) if x.1.is_some() => { - let ((name, _, capture, pos), modules, hash, args_expr, def_val) = x.as_ref(); + let ((name, _, _, pos), modules, hash, args_expr, def_val) = x.as_ref(); self.make_qualified_function_call( scope, mods, state, lib, this_ptr, modules, name, args_expr, *def_val, *hash, - *capture, level, + level, ) .map_err(|err| err.fill_position(*pos)) } diff --git a/src/fn_call.rs b/src/fn_call.rs index aa35b527..437a86dc 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -50,10 +50,6 @@ use crate::stdlib::{ vec::Vec, }; -#[cfg(not(feature = "no_closure"))] -#[cfg(not(feature = "no_function"))] -use crate::stdlib::{collections::HashSet, string::String}; - #[cfg(feature = "no_std")] #[cfg(not(feature = "no_float"))] use num_traits::float::Float; @@ -146,29 +142,6 @@ impl Drop for ArgBackup<'_> { } } -// Add captured variables into scope -#[cfg(not(feature = "no_closure"))] -#[cfg(not(feature = "no_function"))] -fn add_captured_variables_into_scope<'s>( - externals: &HashSet, - captured: Scope<'s>, - scope: &mut Scope<'s>, -) { - captured - .into_iter() - .filter(|ScopeEntry { name, .. }| externals.contains(name.as_ref())) - .for_each( - |ScopeEntry { - name, typ, value, .. - }| { - match typ { - ScopeEntryType::Normal => scope.push(name, value), - ScopeEntryType::Constant => scope.push_constant(name, value), - }; - }, - ); -} - #[inline(always)] pub fn ensure_no_data_race( fn_name: &str, @@ -411,9 +384,27 @@ impl Engine { }), ); + // Merge in encapsulated environment, if any + let mut lib_merged; + + let unified_lib = if let Some(ref env_lib) = fn_def.lib { + if !lib.is_empty() { + // In the special case of the main script not defining any function + env_lib + } else { + lib_merged = lib.clone(); + lib_merged.merge(env_lib); + &lib_merged + } + } else { + lib + }; + // Evaluate the function at one higher level of call depth + let stmt = &fn_def.body; + let result = self - .eval_stmt(scope, mods, state, lib, this_ptr, &fn_def.body, level + 1) + .eval_stmt(scope, mods, state, unified_lib, this_ptr, stmt, level + 1) .or_else(|err| match *err { // Convert return statement to return value EvalAltResult::Return(x, _) => Ok(x), @@ -574,10 +565,27 @@ impl Engine { let scope = &mut Scope::new(); let mods = &mut Imports::new(); - // Add captured variables into scope + // Move captured variables into scope #[cfg(not(feature = "no_closure"))] if let Some(captured) = _capture { - add_captured_variables_into_scope(&func.externals, captured, scope); + captured + .into_iter() + .filter(|ScopeEntry { name, .. }| { + func.externals.contains(name.as_ref()) + }) + .for_each( + |ScopeEntry { + name, typ, value, .. + }| { + // Consume the scope values. + match typ { + ScopeEntryType::Normal => scope.push(name, value), + ScopeEntryType::Constant => { + scope.push_constant(name, value) + } + }; + }, + ); } let result = if _is_method { @@ -1020,7 +1028,7 @@ impl Engine { let mut args: StaticVec<_>; let mut is_ref = false; let capture = if cfg!(not(feature = "no_closure")) && capture && !scope.is_empty() { - Some(scope.flatten_clone()) + Some(scope.clone_visible()) } else { None }; @@ -1092,7 +1100,6 @@ impl Engine { args_expr: &[Expr], def_val: Option, hash_script: u64, - _capture: bool, level: usize, ) -> Result> { let modules = modules.as_ref().unwrap(); @@ -1189,20 +1196,12 @@ impl Engine { let args = args.as_mut(); let func = f.get_fn_def(); - let scope = &mut Scope::new(); + let new_scope = &mut Scope::new(); let mods = &mut Imports::new(); - // Add captured variables into scope - #[cfg(not(feature = "no_closure"))] - if _capture && !scope.is_empty() { - add_captured_variables_into_scope( - &func.externals, - scope.flatten_clone(), - scope, - ); - } - - self.call_script_fn(scope, mods, state, lib, &mut None, name, func, args, level) + self.call_script_fn( + new_scope, mods, state, lib, &mut None, name, func, args, level, + ) } Some(f) if f.is_plugin_fn() => f.get_plugin_fn().call(args.as_mut()), Some(f) if f.is_native() => { diff --git a/src/fn_native.rs b/src/fn_native.rs index 3aa5e7a4..48df3aee 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -9,14 +9,11 @@ use crate::result::EvalAltResult; use crate::scope::Scope; use crate::token::{is_valid_identifier, Position}; use crate::utils::ImmutableString; +use crate::{calc_fn_hash, StaticVec}; -#[cfg(not(feature = "no_function"))] -use crate::{calc_fn_hash, module::FuncReturn, StaticVec}; - -use crate::stdlib::{boxed::Box, convert::TryFrom, fmt, string::String, vec::Vec}; - -#[cfg(not(feature = "no_function"))] -use crate::stdlib::{iter::empty, mem}; +use crate::stdlib::{ + boxed::Box, convert::TryFrom, fmt, iter::empty, mem, string::String, vec::Vec, +}; #[cfg(not(feature = "sync"))] use crate::stdlib::rc::Rc; @@ -114,14 +111,13 @@ impl FnPtr { /// 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. - #[cfg(not(feature = "no_function"))] pub fn call_dynamic( &self, engine: &Engine, lib: impl AsRef, this_ptr: Option<&mut Dynamic>, mut arg_values: impl AsMut<[Dynamic]>, - ) -> FuncReturn { + ) -> Result> { let mut args_data = self .1 .iter() diff --git a/src/module/mod.rs b/src/module/mod.rs index fa509656..afa82cfe 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -288,7 +288,7 @@ impl Module { /// If there is an existing function of the same name and number of arguments, it is replaced. #[cfg(not(feature = "no_function"))] #[inline] - pub(crate) fn set_script_fn(&mut self, fn_def: ScriptFnDef) -> u64 { + pub(crate) fn set_script_fn(&mut self, fn_def: Shared) -> u64 { // None + function name + number of arguments. let num_params = fn_def.params.len(); let hash_script = calc_fn_hash(empty(), &fn_def.name, num_params, empty()); @@ -554,6 +554,7 @@ impl Module { #[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_module"))] #[inline] + #[allow(dead_code)] pub(crate) fn set_raw_fn_as_scripted( &mut self, name: impl Into, @@ -1392,53 +1393,18 @@ impl Module { module.modules.insert(alias.to_string(), m); }); + // Non-private functions defined become module functions #[cfg(not(feature = "no_function"))] { let ast_lib: Shared = ast.lib().clone().into(); ast.iter_functions() - .filter(|(access, _, _, _)| access.is_public()) - .for_each(|(_, name, num_params, func)| { - let ast_lib = ast_lib.clone(); - - module.set_raw_fn_as_scripted( - name, - num_params, - move |engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]| { - let mut lib_merged; - - let unified_lib = if lib.is_empty() { - // In the special case of the main script not defining any function - &ast_lib - } else { - lib_merged = lib.clone(); - lib_merged.merge(&ast_lib); - &lib_merged - }; - - engine - .call_script_fn( - &mut Default::default(), - &mut Default::default(), - &mut Default::default(), - unified_lib, - &mut None, - &func.name, - func.as_ref(), - args, - 0, - ) - .map_err(|err| { - // Wrap the error in a module-error - EvalAltResult::ErrorInModule( - "".to_string(), - err, - Position::none(), - ) - .into() - }) - }, - ); + .filter(|(access, _, _, _)| !access.is_private()) + .for_each(|(_, _, _, func)| { + // Encapsulate AST environment + let mut func = func.as_ref().clone(); + func.lib = Some(ast_lib.clone()); + module.set_script_fn(func.into()); }); } diff --git a/src/optimize.rs b/src/optimize.rs index a314cdac..631f68d6 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -823,6 +823,7 @@ pub fn optimize_into_ast( #[cfg(not(feature = "no_closure"))] externals: fn_def.externals.clone(), pos: fn_def.pos, + lib: None, } .into() }) @@ -862,7 +863,7 @@ pub fn optimize_into_ast( }); } else { _functions.into_iter().for_each(|fn_def| { - module.set_script_fn(fn_def); + module.set_script_fn(fn_def.into()); }); } diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index d1f497f9..107ed85e 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -7,17 +7,13 @@ use crate::engine::{Array, Engine}; use crate::fn_native::FnPtr; use crate::parser::{ImmutableString, INT}; use crate::plugin::*; - -#[cfg(not(feature = "unchecked"))] -use crate::{result::EvalAltResult, token::Position}; +use crate::result::EvalAltResult; +use crate::token::Position; #[cfg(not(feature = "no_object"))] use crate::engine::Map; -use crate::stdlib::{any::TypeId, boxed::Box}; - -#[cfg(not(feature = "unchecked"))] -use crate::stdlib::string::ToString; +use crate::stdlib::{any::TypeId, boxed::Box, cmp::Ordering, string::ToString}; pub type Unit = (); @@ -75,6 +71,14 @@ def_package!(crate:BasicArrayPackage:"Basic array utilities.", lib, { #[cfg(not(feature = "no_object"))] reg_functions!(lib += map; Map); + lib.set_raw_fn("map", &[TypeId::of::(), TypeId::of::()], map); + lib.set_raw_fn("filter", &[TypeId::of::(), TypeId::of::()], filter); + lib.set_raw_fn("reduce", &[TypeId::of::(), TypeId::of::()], reduce); + lib.set_raw_fn("reduce_rev", &[TypeId::of::(), TypeId::of::()], reduce_rev); + lib.set_raw_fn("some", &[TypeId::of::(), TypeId::of::()], some); + lib.set_raw_fn("all", &[TypeId::of::(), TypeId::of::()], all); + lib.set_raw_fn("sort", &[TypeId::of::(), TypeId::of::()], sort); + // Merge in the module at the end to override `+=` for arrays combine_with_exported_module!(lib, "array", array_functions); @@ -130,6 +134,25 @@ mod array_functions { pub fn reverse(list: &mut Array) { list.reverse(); } + pub fn splice(list: &mut Array, start: INT, len: INT, replace: Array) { + let start = if start < 0 { + 0 + } else if start as usize >= list.len() { + list.len() - 1 + } else { + start as usize + }; + + let len = if len < 0 { + 0 + } else if len as usize > list.len() - start { + list.len() - start + } else { + len as usize + }; + + list.splice(start..start + len, replace.into_iter()); + } } fn pad( @@ -165,6 +188,250 @@ fn pad( Ok(()) } +fn map( + engine: &Engine, + lib: &Module, + args: &mut [&mut Dynamic], +) -> Result> { + let list = args[0].read_lock::().unwrap(); + let mapper = args[1].read_lock::().unwrap(); + + let mut array = Array::with_capacity(list.len()); + + for (i, item) in list.iter().enumerate() { + array.push( + mapper + .call_dynamic(engine, lib, None, [item.clone()]) + .or_else(|err| match *err { + EvalAltResult::ErrorFunctionNotFound(_, _) => { + mapper.call_dynamic(engine, lib, None, [item.clone(), (i as INT).into()]) + } + _ => Err(err), + }) + .map_err(|err| { + Box::new(EvalAltResult::ErrorInFunctionCall( + "map".to_string(), + err, + Position::none(), + )) + })?, + ); + } + + Ok(array) +} + +fn filter( + engine: &Engine, + lib: &Module, + args: &mut [&mut Dynamic], +) -> Result> { + let list = args[0].read_lock::().unwrap(); + let filter = args[1].read_lock::().unwrap(); + + let mut array = Array::with_capacity(list.len()); + + for (i, item) in list.iter().enumerate() { + if filter + .call_dynamic(engine, lib, None, [item.clone()]) + .or_else(|err| match *err { + EvalAltResult::ErrorFunctionNotFound(_, _) => { + filter.call_dynamic(engine, lib, None, [item.clone(), (i as INT).into()]) + } + _ => Err(err), + }) + .map_err(|err| { + Box::new(EvalAltResult::ErrorInFunctionCall( + "filter".to_string(), + err, + Position::none(), + )) + })? + .as_bool() + .unwrap_or(false) + { + array.push(item.clone()); + } + } + + Ok(array) +} + +fn some( + engine: &Engine, + lib: &Module, + args: &mut [&mut Dynamic], +) -> Result> { + let list = args[0].read_lock::().unwrap(); + let filter = args[1].read_lock::().unwrap(); + + for (i, item) in list.iter().enumerate() { + if filter + .call_dynamic(engine, lib, None, [item.clone()]) + .or_else(|err| match *err { + EvalAltResult::ErrorFunctionNotFound(_, _) => { + filter.call_dynamic(engine, lib, None, [item.clone(), (i as INT).into()]) + } + _ => Err(err), + }) + .map_err(|err| { + Box::new(EvalAltResult::ErrorInFunctionCall( + "filter".to_string(), + err, + Position::none(), + )) + })? + .as_bool() + .unwrap_or(false) + { + return Ok(true.into()); + } + } + + Ok(false.into()) +} + +fn all( + engine: &Engine, + lib: &Module, + args: &mut [&mut Dynamic], +) -> Result> { + let list = args[0].read_lock::().unwrap(); + let filter = args[1].read_lock::().unwrap(); + + for (i, item) in list.iter().enumerate() { + if !filter + .call_dynamic(engine, lib, None, [item.clone()]) + .or_else(|err| match *err { + EvalAltResult::ErrorFunctionNotFound(_, _) => { + filter.call_dynamic(engine, lib, None, [item.clone(), (i as INT).into()]) + } + _ => Err(err), + }) + .map_err(|err| { + Box::new(EvalAltResult::ErrorInFunctionCall( + "filter".to_string(), + err, + Position::none(), + )) + })? + .as_bool() + .unwrap_or(false) + { + return Ok(false.into()); + } + } + + Ok(true.into()) +} + +fn reduce( + engine: &Engine, + lib: &Module, + args: &mut [&mut Dynamic], +) -> Result> { + let list = args[0].read_lock::().unwrap(); + let reducer = args[1].read_lock::().unwrap(); + + let mut result: Dynamic = ().into(); + + for (i, item) in list.iter().enumerate() { + result = reducer + .call_dynamic(engine, lib, None, [result.clone(), item.clone()]) + .or_else(|err| match *err { + EvalAltResult::ErrorFunctionNotFound(_, _) => reducer.call_dynamic( + engine, + lib, + None, + [result, item.clone(), (i as INT).into()], + ), + _ => Err(err), + }) + .map_err(|err| { + Box::new(EvalAltResult::ErrorInFunctionCall( + "reduce".to_string(), + err, + Position::none(), + )) + })?; + } + + Ok(result) +} + +fn reduce_rev( + engine: &Engine, + lib: &Module, + args: &mut [&mut Dynamic], +) -> Result> { + let list = args[0].read_lock::().unwrap(); + let reducer = args[1].read_lock::().unwrap(); + + let mut result: Dynamic = ().into(); + + for (i, item) in list.iter().enumerate().rev() { + result = reducer + .call_dynamic(engine, lib, None, [result.clone(), item.clone()]) + .or_else(|err| match *err { + EvalAltResult::ErrorFunctionNotFound(_, _) => reducer.call_dynamic( + engine, + lib, + None, + [result, item.clone(), (i as INT).into()], + ), + _ => Err(err), + }) + .map_err(|err| { + Box::new(EvalAltResult::ErrorInFunctionCall( + "reduce".to_string(), + err, + Position::none(), + )) + })?; + } + + Ok(result) +} + +fn sort( + engine: &Engine, + lib: &Module, + args: &mut [&mut Dynamic], +) -> Result> { + let comparer = args[1].read_lock::().unwrap().clone(); + let mut list = args[0].write_lock::().unwrap(); + + list.sort_by(|x, y| { + comparer + .call_dynamic(engine, lib, None, [x.clone(), y.clone()]) + .ok() + .and_then(|v| v.as_int().ok()) + .map(|v| { + if v > 0 { + Ordering::Greater + } else if v < 0 { + Ordering::Less + } else { + Ordering::Equal + } + }) + .unwrap_or_else(|| { + let x_type_id = x.type_id(); + let y_type_id = y.type_id(); + + if x_type_id > y_type_id { + Ordering::Greater + } else if x_type_id < y_type_id { + Ordering::Less + } else { + Ordering::Equal + } + }) + }); + + Ok(().into()) +} + gen_array_functions!(basic => INT, bool, char, ImmutableString, FnPtr, Array, Unit); #[cfg(not(feature = "only_i32"))] diff --git a/src/parser.rs b/src/parser.rs index 4e932fe9..77b460f7 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -549,6 +549,8 @@ pub struct ScriptFnDef { pub body: Stmt, /// Position of the function definition. pub pos: Position, + /// Encapsulated running environment, if any. + pub lib: Option>, } impl fmt::Display for ScriptFnDef { @@ -1949,7 +1951,17 @@ fn parse_primary( settings.pos = token_pos; root_expr = match (root_expr, token) { - // Function call + // Qualified function call with ! + #[cfg(not(feature = "no_closure"))] + (Expr::Variable(x), Token::Bang) if x.1.is_some() => { + return Err(if !match_token(input, Token::LeftParen)? { + LexError::UnexpectedInput(Token::Bang.syntax().to_string()).into_err(token_pos) + } else { + PERR::BadInput("'!' cannot be used to call module functions".to_string()) + .into_err(token_pos) + }); + } + // Function call with ! #[cfg(not(feature = "no_closure"))] (Expr::Variable(x), Token::Bang) => { if !match_token(input, Token::LeftParen)? { @@ -3363,6 +3375,7 @@ fn parse_fn( externals, body, pos: settings.pos, + lib: None, }) } @@ -3540,6 +3553,7 @@ fn parse_anon_fn( externals: Default::default(), body, pos: settings.pos, + lib: None, }; let expr = Expr::FnPointer(Box::new((fn_name, settings.pos))); diff --git a/src/result.rs b/src/result.rs index b5191cb4..ad73bae2 100644 --- a/src/result.rs +++ b/src/result.rs @@ -5,6 +5,9 @@ use crate::error::ParseErrorType; use crate::parser::INT; use crate::token::Position; +#[cfg(not(feature = "no_function"))] +use crate::engine::is_anonymous_fn; + use crate::stdlib::{ boxed::Box, error::Error, @@ -166,6 +169,10 @@ impl fmt::Display for EvalAltResult { Self::ErrorParsing(p, _) => write!(f, "Syntax error: {}", p)?, + #[cfg(not(feature = "no_function"))] + Self::ErrorInFunctionCall(s, err, _) if is_anonymous_fn(s) => { + write!(f, "Error in call to closure: {}", err)? + } Self::ErrorInFunctionCall(s, err, _) => { write!(f, "Error in call to function '{}': {}", s, err)? } diff --git a/src/scope.rs b/src/scope.rs index ad021f7e..c1c78181 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -420,7 +420,7 @@ impl<'a> Scope<'a> { /// Clone the Scope, keeping only the last instances of each variable name. /// Shadowed variables are omitted in the copy. #[inline] - pub(crate) fn flatten_clone(&self) -> Self { + pub(crate) fn clone_visible(&self) -> Self { let mut entries: Vec = Default::default(); self.0.iter().rev().for_each(|entry| { diff --git a/src/token.rs b/src/token.rs index 181524cc..fde6a703 100644 --- a/src/token.rs +++ b/src/token.rs @@ -623,7 +623,9 @@ impl Token { Plus | Minus => 150, - Divide | Multiply | PowerOf | Modulo => 180, + Divide | Multiply => 180, + + PowerOf | Modulo => 190, LeftShift | RightShift => 210, diff --git a/tests/arrays.rs b/tests/arrays.rs index 62d9de0b..cbd4daf5 100644 --- a/tests/arrays.rs +++ b/tests/arrays.rs @@ -112,3 +112,125 @@ fn test_array_with_structs() -> Result<(), Box> { Ok(()) } + +#[cfg(not(feature = "no_object"))] +#[cfg(not(feature = "no_function"))] +#[cfg(not(feature = "no_closure"))] +#[test] +fn test_arrays_map_reduce() -> Result<(), Box> { + let engine = Engine::new(); + + assert_eq!( + engine.eval::( + r" + let x = [1, 2, 3]; + let y = x.filter(|v| v > 2); + y[0] + " + )?, + 3 + ); + + assert_eq!( + engine.eval::( + r" + let x = [1, 2, 3]; + let y = x.filter(|v, i| v > i); + y.len() + " + )?, + 3 + ); + + assert_eq!( + engine.eval::( + r" + let x = [1, 2, 3]; + let y = x.map(|v| v * 2); + y[2] + " + )?, + 6 + ); + + assert_eq!( + engine.eval::( + r" + let x = [1, 2, 3]; + let y = x.map(|v, i| v * i); + y[2] + " + )?, + 6 + ); + + assert_eq!( + engine.eval::( + r#" + let x = [1, 2, 3]; + x.reduce(|sum, v| if sum.type_of() == "()" { v * v } else { sum + v * v }) + "# + )?, + 14 + ); + + assert_eq!( + engine.eval::( + r#" + let x = [1, 2, 3]; + x.reduce(|sum, v, i| { if i == 0 { sum = 10 } sum + v * v }) + "# + )?, + 24 + ); + + assert_eq!( + engine.eval::( + r#" + let x = [1, 2, 3]; + x.reduce_rev(|sum, v| if sum.type_of() == "()" { v * v } else { sum + v * v }) + "# + )?, + 14 + ); + + assert_eq!( + engine.eval::( + r#" + let x = [1, 2, 3]; + x.reduce_rev(|sum, v, i| { if i == 2 { sum = 10 } sum + v * v }) + "# + )?, + 24 + ); + + assert!(engine.eval::( + r#" + let x = [1, 2, 3]; + x.some(|v| v > 1) + "# + )?); + + assert!(engine.eval::( + r#" + let x = [1, 2, 3]; + x.some(|v, i| v * i == 0) + "# + )?); + + assert!(!engine.eval::( + r#" + let x = [1, 2, 3]; + x.all(|v| v > 1) + "# + )?); + + assert!(engine.eval::( + r#" + let x = [1, 2, 3]; + x.all(|v, i| v > i) + "# + )?); + + Ok(()) +}