Merge pull request #257 from schungx/master

New methods for arrays.
This commit is contained in:
Stephen Chung 2020-10-13 13:28:30 +08:00 committed by GitHub
commit 737c0aca44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 662 additions and 197 deletions

View File

@ -4,6 +4,8 @@ Rhai Release Notes
Version 0.20.0 Version 0.20.0
============== ==============
This version adds a variable resolver with the ability to short-circuit variable access.
Breaking changes Breaking changes
---------------- ----------------
@ -17,6 +19,8 @@ Breaking changes
* `rhai::ser` and `rhai::de` namespaces are merged into `rhai::serde`. * `rhai::ser` and `rhai::de` namespaces are merged into `rhai::serde`.
* New reserved symbols: `++`, `--`, `..`, `...`. * New reserved symbols: `++`, `--`, `..`, `...`.
* Callback signature for custom syntax implementation function is changed to allow for more flexibility. * 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 New features
------------ ------------
@ -24,15 +28,15 @@ New features
* New `Engine::on_var` to register a _variable resolver_. * 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. * `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). * `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. * `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. * `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 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. * 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 Version 0.19.0

View File

@ -17,6 +17,8 @@ Easy
* Very few additional dependencies - right now only [`smallvec`](https://crates.io/crates/smallvec/) plus crates for procedural macros; * 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. 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 Fast
---- ----
@ -41,6 +43,8 @@ Dynamic
* Some support for [object-oriented programming (OOP)][OOP]. * Some support for [object-oriented programming (OOP)][OOP].
* Hook into variables access via [variable resolver].
Safe Safe
---- ----
@ -66,9 +70,6 @@ Flexible
* Supports [most build targets](targets.md) including `no-std` and [WASM]. * 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. * Surgically [disable keywords and operators] to restrict the language.
* Use as a [DSL] by [disabling keywords/operators][disable keywords and operators], [custom operators] * Use as a [DSL] by defining [custom operators] and/or extending the language with [custom syntax].
and extending the language with [custom syntax].

View File

@ -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 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. 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] 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. integrate with native Rust applications.
* No formal language grammar - Rhai uses a hand-coded lexer, a hand-coded top-down recursive-descent parser * 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 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], [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 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, advanced language features such as classes, inheritance, interfaces, generics,
concurrency, byte-codes VM, JIT etc. first-class functions/closures, pattern matching, concurrency, byte-codes VM, JIT etc.
Avoid the temptation to write full-fledge application logic entirely in Rhai - 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. that use case is best fulfilled by more complete languages such as JavaScript or Lua.

View File

@ -95,7 +95,8 @@ The following _precedence table_ show the built-in precedence of standard Rhai o
| Comparisons | `>`, `>=`, `<`, `<=` | 110 | | Comparisons | `>`, `>=`, `<`, `<=` | 110 |
| | `in` | 130 | | | `in` | 130 |
| Arithmetic | `+`, `-` | 150 | | Arithmetic | `+`, `-` | 150 |
| Arithmetic | `*`, `/`, `~`, `%` | 180 | | Arithmetic | `*`, `/` | 180 |
| Arithmetic | `~`, `%` | 190 |
| Bit-shifts | `<<`, `>>` | 210 | | Bit-shifts | `<<`, `>>` | 210 |
| Object | `.` _(binds to right)_ | 240 | | Object | `.` _(binds to right)_ | 240 |
| _Others_ | | 0 | | _Others_ | | 0 |

View File

@ -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 There is a built-in facility for advanced users to _hook_ into the variable
resolution service and to override its default behavior. 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 ```rust
let mut engine = Engine::new(); 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. 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 Function Signature
------------------ ------------------
@ -68,5 +80,13 @@ where:
* `context.namespace(): &Module` - reference to the current _global namespace_ (as a [module]) containing all script-defined functions. * `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. * `context.call_level(): usize` - the current nesting level of function calls.
The return value is `Result<Option<Dynamic>, Box<EvalAltResult>>` where `Ok(None)` indicates that the normal ### Return Value
variable resolution process should continue.
The return value is `Result<Option<Dynamic>, Box<EvalAltResult>>` 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<EvalAltResult>)` - 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.

View File

@ -31,7 +31,7 @@ Built-in Functions
The following methods (mostly defined in the [`BasicArrayPackage`][packages] but excluded if using a [raw `Engine`]) operate on arrays: The following methods (mostly defined in the [`BasicArrayPackage`][packages] but excluded if using a [raw `Engine`]) operate on arrays:
| Function | Parameter(s) | Description | | Function | Parameter(s) | Description |
| ------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `push` | element to insert | inserts an element at the end | | `push` | element to insert | inserts an element at the end |
| `append` | array to append | concatenates the second array to the end of the first | | `append` | array to append | concatenates the second array to the end of the first |
| `+=` operator | 1) array<br/>2) element to insert (not another array) | inserts an element at the end | | `+=` operator | 1) array<br/>2) element to insert (not another array) | inserts an element at the end |
@ -40,12 +40,20 @@ The following methods (mostly defined in the [`BasicArrayPackage`][packages] but
| `insert` | 1) element to insert<br/>2) position (beginning if <= 0, end if >= length) | inserts an element at a certain index | | `insert` | 1) element to insert<br/>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) | | `pop` | _none_ | removes the last element and returns it ([`()`] if empty) |
| `shift` | _none_ | removes the first 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 | | `remove` | index | removes an element at a particular index and returns it ([`()`] if the index is not valid) |
| `reverse` | _none_ | reverses the array | | `reverse` | _none_ | reverses the array |
| `len` method and property | _none_ | returns the number of elements | | `len` method and property | _none_ | returns the number of elements |
| `pad` | 1) target length<br/>2) element to pad | pads the array with an element to at least a specified length | | `pad` | 1) target length<br/>2) element to pad | pads the array with an element to at least a specified length |
| `clear` | _none_ | empties the array | | `clear` | _none_ | empties the array |
| `truncate` | target length | cuts off the array at exactly a specified length (discarding all subsequent elements) | | `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),<br/>2) number of items to remove (none if <= 0),<br/>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:<br/>1st parameter: array item,<br/>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:<br/>1st parameter: array item,<br/>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:<br/>1st parameter: accumulated value ([`()`] initially),<br/>2nd parameter: array item,<br/>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:<br/>1st parameter: accumulated value ([`()`] initially),<br/>2nd parameter: array item,<br/>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:<br/>1st parameter: array item,<br/>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:<br/>1st parameter: array item,<br/>2nd parameter: offset index (optional) |
| `sort` | [function pointer] to a comparison function (can be a [closure]) | sorts the array with a comparison function:<br/>1st parameter: first item,<br/>2nd parameter: second item |
Use Custom Types With Arrays Use Custom Types With Arrays
@ -136,4 +144,48 @@ y.len == 5;
y.clear(); // empty the array y.clear(); // empty the array
y.len == 0; 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];
``` ```

View File

@ -43,6 +43,12 @@ let f = Fn("foo");
call!(f, 41) == 42; // must use function-call style call!(f, 41) == 42; // must use function-call style
f.call!(41); // <- syntax error: capturing is not allowed in method-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
``` ```

View File

@ -35,8 +35,8 @@ Implementation
let mut engine = Engine::new(); let mut engine = Engine::new();
// Create shared data provider. // Create shared data provider.
// Assume that Provider::get(&str) -> Option<value> gets a system constant. // Assume that SystemValuesProvider::get(&str) -> Option<value> gets a value.
let provider: Arc<Provider> = get_data_provider(); let provider = Arc::new(SystemValuesProvider::new());
// Clone the shared provider // Clone the shared provider
let db = provider.clone(); let db = provider.clone();

View File

@ -37,6 +37,7 @@ use crate::stdlib::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
fmt, format, fmt, format,
iter::{empty, once}, iter::{empty, once},
num::NonZeroUsize,
ops::DerefMut, ops::DerefMut,
string::{String, ToString}, string::{String, ToString},
vec::Vec, vec::Vec,
@ -250,8 +251,8 @@ impl<'a> Target<'a> {
Self::LockGuard(_) => (), Self::LockGuard(_) => (),
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Self::StringChar(_, _, ch) => { Self::StringChar(_, _, ch) => {
let new_val = ch.clone(); let char_value = ch.clone();
self.set_value((new_val, Position::none()), Position::none()) self.set_value((char_value, Position::none()), Position::none())
.unwrap(); .unwrap();
} }
} }
@ -284,10 +285,9 @@ impl<'a> Target<'a> {
})?; })?;
let mut chars = s.chars().collect::<StaticVec<_>>(); let mut chars = s.chars().collect::<StaticVec<_>>();
let ch = chars[*index];
// See if changed - if so, update the String // See if changed - if so, update the String
if ch != new_ch { if chars[*index] != new_ch {
chars[*index] = new_ch; chars[*index] = new_ch;
*s = chars.iter().collect::<String>().into(); *s = chars.iter().collect::<String>().into();
} }
@ -502,6 +502,13 @@ pub fn make_setter(id: &str) -> String {
format!("{}{}", FN_SET, id) 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 /// Print/debug to stdout
fn default_print(_s: &str) { fn default_print(_s: &str) {
#[cfg(not(feature = "no_std"))] #[cfg(not(feature = "no_std"))]
@ -520,13 +527,13 @@ pub fn search_imports<'s>(
// Qualified - check if the root module is directly indexed // Qualified - check if the root module is directly indexed
let index = if state.always_search { let index = if state.always_search {
None 0
} else { } else {
modules.index() modules.index().map_or(0, NonZeroUsize::get)
}; };
Ok(if let Some(index) = index { Ok(if index > 0 {
let offset = mods.len() - index.get(); let offset = mods.len() - index;
&mods.get(offset).unwrap().1 &mods.get(offset).unwrap().1
} else { } else {
mods.iter() mods.iter()
@ -548,13 +555,13 @@ pub fn search_imports_mut<'s>(
// Qualified - check if the root module is directly indexed // Qualified - check if the root module is directly indexed
let index = if state.always_search { let index = if state.always_search {
None 0
} else { } else {
modules.index() modules.index().map_or(0, NonZeroUsize::get)
}; };
Ok(if let Some(index) = index { Ok(if index > 0 {
let offset = mods.len() - index.get(); let offset = mods.len() - index;
&mut mods.get_mut(offset).unwrap().1 &mut mods.get_mut(offset).unwrap().1
} else { } else {
mods.iter_mut() mods.iter_mut()
@ -684,18 +691,14 @@ impl Engine {
// Qualified variable // Qualified variable
((name, pos), Some(modules), hash_var, _) => { ((name, pos), Some(modules), hash_var, _) => {
let module = search_imports_mut(mods, state, modules)?; let module = search_imports_mut(mods, state, modules)?;
let target = let target = module.get_qualified_var_mut(*hash_var).map_err(|mut err| {
module match *err {
.get_qualified_var_mut(*hash_var) EvalAltResult::ErrorVariableNotFound(ref mut err_name, _) => {
.map_err(|err| match *err { *err_name = format!("{}{}", modules, name);
EvalAltResult::ErrorVariableNotFound(_, _) => {
EvalAltResult::ErrorVariableNotFound(
format!("{}{}", modules, name),
*pos,
)
.into()
} }
_ => err.fill_position(*pos), _ => (),
}
err.fill_position(*pos)
})?; })?;
// Module variables are constant // Module variables are constant
@ -733,7 +736,11 @@ impl Engine {
} }
// Check if it is directly indexed // 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 // Check the variable resolver, if any
if let Some(ref resolve_var) = self.resolve_var { if let Some(ref resolve_var) = self.resolve_var {
@ -745,15 +752,15 @@ impl Engine {
this_ptr, this_ptr,
level: 0, level: 0,
}; };
if let Some(result) = resolve_var(name, index.map_or(0, |v| v.get()), scope, &context) if let Some(result) =
.map_err(|err| err.fill_position(*pos))? resolve_var(name, index, scope, &context).map_err(|err| err.fill_position(*pos))?
{ {
return Ok((result.into(), name, ScopeEntryType::Constant, *pos)); return Ok((result.into(), name, ScopeEntryType::Constant, *pos));
} }
} }
let index = if let Some(index) = index { let index = if index > 0 {
scope.len() - index.get() scope.len() - index
} else { } else {
// Find the variable in the scope // Find the variable in the scope
scope scope
@ -1604,10 +1611,10 @@ impl Engine {
// Module-qualified function call // Module-qualified function call
Expr::FnCall(x) if x.1.is_some() => { 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( self.make_qualified_function_call(
scope, mods, state, lib, this_ptr, modules, name, args_expr, *def_val, *hash, scope, mods, state, lib, this_ptr, modules, name, args_expr, *def_val, *hash,
*capture, level, level,
) )
.map_err(|err| err.fill_position(*pos)) .map_err(|err| err.fill_position(*pos))
} }

View File

@ -50,10 +50,6 @@ use crate::stdlib::{
vec::Vec, vec::Vec,
}; };
#[cfg(not(feature = "no_closure"))]
#[cfg(not(feature = "no_function"))]
use crate::stdlib::{collections::HashSet, string::String};
#[cfg(feature = "no_std")] #[cfg(feature = "no_std")]
#[cfg(not(feature = "no_float"))] #[cfg(not(feature = "no_float"))]
use num_traits::float::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<String>,
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)] #[inline(always)]
pub fn ensure_no_data_race( pub fn ensure_no_data_race(
fn_name: &str, 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 // Evaluate the function at one higher level of call depth
let stmt = &fn_def.body;
let result = self 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 { .or_else(|err| match *err {
// Convert return statement to return value // Convert return statement to return value
EvalAltResult::Return(x, _) => Ok(x), EvalAltResult::Return(x, _) => Ok(x),
@ -574,10 +565,27 @@ impl Engine {
let scope = &mut Scope::new(); let scope = &mut Scope::new();
let mods = &mut Imports::new(); let mods = &mut Imports::new();
// Add captured variables into scope // Move captured variables into scope
#[cfg(not(feature = "no_closure"))] #[cfg(not(feature = "no_closure"))]
if let Some(captured) = _capture { 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 { let result = if _is_method {
@ -1020,7 +1028,7 @@ impl Engine {
let mut args: StaticVec<_>; let mut args: StaticVec<_>;
let mut is_ref = false; let mut is_ref = false;
let capture = if cfg!(not(feature = "no_closure")) && capture && !scope.is_empty() { let capture = if cfg!(not(feature = "no_closure")) && capture && !scope.is_empty() {
Some(scope.flatten_clone()) Some(scope.clone_visible())
} else { } else {
None None
}; };
@ -1092,7 +1100,6 @@ impl Engine {
args_expr: &[Expr], args_expr: &[Expr],
def_val: Option<bool>, def_val: Option<bool>,
hash_script: u64, hash_script: u64,
_capture: bool,
level: usize, level: usize,
) -> Result<Dynamic, Box<EvalAltResult>> { ) -> Result<Dynamic, Box<EvalAltResult>> {
let modules = modules.as_ref().unwrap(); let modules = modules.as_ref().unwrap();
@ -1189,20 +1196,12 @@ impl Engine {
let args = args.as_mut(); let args = args.as_mut();
let func = f.get_fn_def(); let func = f.get_fn_def();
let scope = &mut Scope::new(); let new_scope = &mut Scope::new();
let mods = &mut Imports::new(); let mods = &mut Imports::new();
// Add captured variables into scope self.call_script_fn(
#[cfg(not(feature = "no_closure"))] new_scope, mods, state, lib, &mut None, name, func, args, level,
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)
} }
Some(f) if f.is_plugin_fn() => f.get_plugin_fn().call(args.as_mut()), Some(f) if f.is_plugin_fn() => f.get_plugin_fn().call(args.as_mut()),
Some(f) if f.is_native() => { Some(f) if f.is_native() => {

View File

@ -9,14 +9,11 @@ use crate::result::EvalAltResult;
use crate::scope::Scope; use crate::scope::Scope;
use crate::token::{is_valid_identifier, Position}; use crate::token::{is_valid_identifier, Position};
use crate::utils::ImmutableString; use crate::utils::ImmutableString;
use crate::{calc_fn_hash, StaticVec};
#[cfg(not(feature = "no_function"))] use crate::stdlib::{
use crate::{calc_fn_hash, module::FuncReturn, StaticVec}; boxed::Box, convert::TryFrom, fmt, iter::empty, mem, string::String, vec::Vec,
};
use crate::stdlib::{boxed::Box, convert::TryFrom, fmt, string::String, vec::Vec};
#[cfg(not(feature = "no_function"))]
use crate::stdlib::{iter::empty, mem};
#[cfg(not(feature = "sync"))] #[cfg(not(feature = "sync"))]
use crate::stdlib::rc::Rc; use crate::stdlib::rc::Rc;
@ -114,14 +111,13 @@ impl FnPtr {
/// This is to avoid unnecessarily cloning the arguments. /// This is to avoid unnecessarily cloning the arguments.
/// Do not use the arguments after this call. If they are needed afterwards, /// Do not use the arguments after this call. If they are needed afterwards,
/// clone them _before_ calling this function. /// clone them _before_ calling this function.
#[cfg(not(feature = "no_function"))]
pub fn call_dynamic( pub fn call_dynamic(
&self, &self,
engine: &Engine, engine: &Engine,
lib: impl AsRef<Module>, lib: impl AsRef<Module>,
this_ptr: Option<&mut Dynamic>, this_ptr: Option<&mut Dynamic>,
mut arg_values: impl AsMut<[Dynamic]>, mut arg_values: impl AsMut<[Dynamic]>,
) -> FuncReturn<Dynamic> { ) -> Result<Dynamic, Box<EvalAltResult>> {
let mut args_data = self let mut args_data = self
.1 .1
.iter() .iter()

View File

@ -288,7 +288,7 @@ impl Module {
/// If there is an existing function of the same name and number of arguments, it is replaced. /// If there is an existing function of the same name and number of arguments, it is replaced.
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
#[inline] #[inline]
pub(crate) fn set_script_fn(&mut self, fn_def: ScriptFnDef) -> u64 { pub(crate) fn set_script_fn(&mut self, fn_def: Shared<ScriptFnDef>) -> u64 {
// None + function name + number of arguments. // None + function name + number of arguments.
let num_params = fn_def.params.len(); let num_params = fn_def.params.len();
let hash_script = calc_fn_hash(empty(), &fn_def.name, num_params, empty()); 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_function"))]
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
#[inline] #[inline]
#[allow(dead_code)]
pub(crate) fn set_raw_fn_as_scripted( pub(crate) fn set_raw_fn_as_scripted(
&mut self, &mut self,
name: impl Into<String>, name: impl Into<String>,
@ -1392,53 +1393,18 @@ impl Module {
module.modules.insert(alias.to_string(), m); module.modules.insert(alias.to_string(), m);
}); });
// Non-private functions defined become module functions
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
{ {
let ast_lib: Shared<Module> = ast.lib().clone().into(); let ast_lib: Shared<Module> = ast.lib().clone().into();
ast.iter_functions() ast.iter_functions()
.filter(|(access, _, _, _)| access.is_public()) .filter(|(access, _, _, _)| !access.is_private())
.for_each(|(_, name, num_params, func)| { .for_each(|(_, _, _, func)| {
let ast_lib = ast_lib.clone(); // Encapsulate AST environment
let mut func = func.as_ref().clone();
module.set_raw_fn_as_scripted( func.lib = Some(ast_lib.clone());
name, module.set_script_fn(func.into());
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()
})
},
);
}); });
} }

View File

@ -823,6 +823,7 @@ pub fn optimize_into_ast(
#[cfg(not(feature = "no_closure"))] #[cfg(not(feature = "no_closure"))]
externals: fn_def.externals.clone(), externals: fn_def.externals.clone(),
pos: fn_def.pos, pos: fn_def.pos,
lib: None,
} }
.into() .into()
}) })
@ -862,7 +863,7 @@ pub fn optimize_into_ast(
}); });
} else { } else {
_functions.into_iter().for_each(|fn_def| { _functions.into_iter().for_each(|fn_def| {
module.set_script_fn(fn_def); module.set_script_fn(fn_def.into());
}); });
} }

View File

@ -7,17 +7,13 @@ use crate::engine::{Array, Engine};
use crate::fn_native::FnPtr; use crate::fn_native::FnPtr;
use crate::parser::{ImmutableString, INT}; use crate::parser::{ImmutableString, INT};
use crate::plugin::*; use crate::plugin::*;
use crate::result::EvalAltResult;
#[cfg(not(feature = "unchecked"))] use crate::token::Position;
use crate::{result::EvalAltResult, token::Position};
#[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_object"))]
use crate::engine::Map; use crate::engine::Map;
use crate::stdlib::{any::TypeId, boxed::Box}; use crate::stdlib::{any::TypeId, boxed::Box, cmp::Ordering, string::ToString};
#[cfg(not(feature = "unchecked"))]
use crate::stdlib::string::ToString;
pub type Unit = (); pub type Unit = ();
@ -75,6 +71,14 @@ def_package!(crate:BasicArrayPackage:"Basic array utilities.", lib, {
#[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_object"))]
reg_functions!(lib += map; Map); reg_functions!(lib += map; Map);
lib.set_raw_fn("map", &[TypeId::of::<Array>(), TypeId::of::<FnPtr>()], map);
lib.set_raw_fn("filter", &[TypeId::of::<Array>(), TypeId::of::<FnPtr>()], filter);
lib.set_raw_fn("reduce", &[TypeId::of::<Array>(), TypeId::of::<FnPtr>()], reduce);
lib.set_raw_fn("reduce_rev", &[TypeId::of::<Array>(), TypeId::of::<FnPtr>()], reduce_rev);
lib.set_raw_fn("some", &[TypeId::of::<Array>(), TypeId::of::<FnPtr>()], some);
lib.set_raw_fn("all", &[TypeId::of::<Array>(), TypeId::of::<FnPtr>()], all);
lib.set_raw_fn("sort", &[TypeId::of::<Array>(), TypeId::of::<FnPtr>()], sort);
// Merge in the module at the end to override `+=` for arrays // Merge in the module at the end to override `+=` for arrays
combine_with_exported_module!(lib, "array", array_functions); combine_with_exported_module!(lib, "array", array_functions);
@ -130,6 +134,25 @@ mod array_functions {
pub fn reverse(list: &mut Array) { pub fn reverse(list: &mut Array) {
list.reverse(); 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<T: Variant + Clone>( fn pad<T: Variant + Clone>(
@ -165,6 +188,250 @@ fn pad<T: Variant + Clone>(
Ok(()) Ok(())
} }
fn map(
engine: &Engine,
lib: &Module,
args: &mut [&mut Dynamic],
) -> Result<Array, Box<EvalAltResult>> {
let list = args[0].read_lock::<Array>().unwrap();
let mapper = args[1].read_lock::<FnPtr>().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<Array, Box<EvalAltResult>> {
let list = args[0].read_lock::<Array>().unwrap();
let filter = args[1].read_lock::<FnPtr>().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<bool, Box<EvalAltResult>> {
let list = args[0].read_lock::<Array>().unwrap();
let filter = args[1].read_lock::<FnPtr>().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<bool, Box<EvalAltResult>> {
let list = args[0].read_lock::<Array>().unwrap();
let filter = args[1].read_lock::<FnPtr>().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<Dynamic, Box<EvalAltResult>> {
let list = args[0].read_lock::<Array>().unwrap();
let reducer = args[1].read_lock::<FnPtr>().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<Dynamic, Box<EvalAltResult>> {
let list = args[0].read_lock::<Array>().unwrap();
let reducer = args[1].read_lock::<FnPtr>().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<Dynamic, Box<EvalAltResult>> {
let comparer = args[1].read_lock::<FnPtr>().unwrap().clone();
let mut list = args[0].write_lock::<Array>().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); gen_array_functions!(basic => INT, bool, char, ImmutableString, FnPtr, Array, Unit);
#[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i32"))]

View File

@ -549,6 +549,8 @@ pub struct ScriptFnDef {
pub body: Stmt, pub body: Stmt,
/// Position of the function definition. /// Position of the function definition.
pub pos: Position, pub pos: Position,
/// Encapsulated running environment, if any.
pub lib: Option<Shared<Module>>,
} }
impl fmt::Display for ScriptFnDef { impl fmt::Display for ScriptFnDef {
@ -1949,7 +1951,17 @@ fn parse_primary(
settings.pos = token_pos; settings.pos = token_pos;
root_expr = match (root_expr, token) { 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"))] #[cfg(not(feature = "no_closure"))]
(Expr::Variable(x), Token::Bang) => { (Expr::Variable(x), Token::Bang) => {
if !match_token(input, Token::LeftParen)? { if !match_token(input, Token::LeftParen)? {
@ -3363,6 +3375,7 @@ fn parse_fn(
externals, externals,
body, body,
pos: settings.pos, pos: settings.pos,
lib: None,
}) })
} }
@ -3540,6 +3553,7 @@ fn parse_anon_fn(
externals: Default::default(), externals: Default::default(),
body, body,
pos: settings.pos, pos: settings.pos,
lib: None,
}; };
let expr = Expr::FnPointer(Box::new((fn_name, settings.pos))); let expr = Expr::FnPointer(Box::new((fn_name, settings.pos)));

View File

@ -5,6 +5,9 @@ use crate::error::ParseErrorType;
use crate::parser::INT; use crate::parser::INT;
use crate::token::Position; use crate::token::Position;
#[cfg(not(feature = "no_function"))]
use crate::engine::is_anonymous_fn;
use crate::stdlib::{ use crate::stdlib::{
boxed::Box, boxed::Box,
error::Error, error::Error,
@ -166,6 +169,10 @@ impl fmt::Display for EvalAltResult {
Self::ErrorParsing(p, _) => write!(f, "Syntax error: {}", p)?, 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, _) => { Self::ErrorInFunctionCall(s, err, _) => {
write!(f, "Error in call to function '{}': {}", s, err)? write!(f, "Error in call to function '{}': {}", s, err)?
} }

View File

@ -420,7 +420,7 @@ impl<'a> Scope<'a> {
/// Clone the Scope, keeping only the last instances of each variable name. /// Clone the Scope, keeping only the last instances of each variable name.
/// Shadowed variables are omitted in the copy. /// Shadowed variables are omitted in the copy.
#[inline] #[inline]
pub(crate) fn flatten_clone(&self) -> Self { pub(crate) fn clone_visible(&self) -> Self {
let mut entries: Vec<Entry> = Default::default(); let mut entries: Vec<Entry> = Default::default();
self.0.iter().rev().for_each(|entry| { self.0.iter().rev().for_each(|entry| {

View File

@ -623,7 +623,9 @@ impl Token {
Plus | Minus => 150, Plus | Minus => 150,
Divide | Multiply | PowerOf | Modulo => 180, Divide | Multiply => 180,
PowerOf | Modulo => 190,
LeftShift | RightShift => 210, LeftShift | RightShift => 210,

View File

@ -112,3 +112,125 @@ fn test_array_with_structs() -> Result<(), Box<EvalAltResult>> {
Ok(()) Ok(())
} }
#[cfg(not(feature = "no_object"))]
#[cfg(not(feature = "no_function"))]
#[cfg(not(feature = "no_closure"))]
#[test]
fn test_arrays_map_reduce() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
assert_eq!(
engine.eval::<INT>(
r"
let x = [1, 2, 3];
let y = x.filter(|v| v > 2);
y[0]
"
)?,
3
);
assert_eq!(
engine.eval::<INT>(
r"
let x = [1, 2, 3];
let y = x.filter(|v, i| v > i);
y.len()
"
)?,
3
);
assert_eq!(
engine.eval::<INT>(
r"
let x = [1, 2, 3];
let y = x.map(|v| v * 2);
y[2]
"
)?,
6
);
assert_eq!(
engine.eval::<INT>(
r"
let x = [1, 2, 3];
let y = x.map(|v, i| v * i);
y[2]
"
)?,
6
);
assert_eq!(
engine.eval::<INT>(
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::<INT>(
r#"
let x = [1, 2, 3];
x.reduce(|sum, v, i| { if i == 0 { sum = 10 } sum + v * v })
"#
)?,
24
);
assert_eq!(
engine.eval::<INT>(
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::<INT>(
r#"
let x = [1, 2, 3];
x.reduce_rev(|sum, v, i| { if i == 2 { sum = 10 } sum + v * v })
"#
)?,
24
);
assert!(engine.eval::<bool>(
r#"
let x = [1, 2, 3];
x.some(|v| v > 1)
"#
)?);
assert!(engine.eval::<bool>(
r#"
let x = [1, 2, 3];
x.some(|v, i| v * i == 0)
"#
)?);
assert!(!engine.eval::<bool>(
r#"
let x = [1, 2, 3];
x.all(|v| v > 1)
"#
)?);
assert!(engine.eval::<bool>(
r#"
let x = [1, 2, 3];
x.all(|v, i| v > i)
"#
)?);
Ok(())
}