From 994e5a4251243c04238a6d2602deb44a25e7f62b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 10 Oct 2020 22:13:55 +0800 Subject: [PATCH 1/4] Move some concat functions to builtin. --- doc/src/engine/raw.md | 18 +++++++-------- doc/src/rust/print-custom.md | 16 ++++++------- src/fn_call.rs | 44 ++++++++++++++++++++++++++++++++++-- src/packages/string_more.rs | 11 +-------- 4 files changed, 60 insertions(+), 29 deletions(-) diff --git a/doc/src/engine/raw.md b/doc/src/engine/raw.md index cf867246..e5add617 100644 --- a/doc/src/engine/raw.md +++ b/doc/src/engine/raw.md @@ -17,12 +17,12 @@ To add more functionalities to a _raw_ `Engine`, load [packages] into it. Built-in Operators ------------------ -| Operators | Assignment operators | Supported for types (see [standard types]) | -| ------------------------ | ---------------------------- | ----------------------------------------------------------------------------- | -| `+`, | `+=` | `INT`, `FLOAT` (if not [`no_float`]), `ImmutableString` | -| `-`, `*`, `/`, `%`, `~`, | `-=`, `*=`, `/=`, `%=`, `~=` | `INT`, `FLOAT` (if not [`no_float`]) | -| `<<`, `>>`, `^`, | `<<=`, `>>=`, `^=` | `INT` | -| `&`, \|, | `&=`, \|= | `INT`, `bool` | -| `&&`, \|\| | | `bool` | -| `==`, `!=` | | `INT`, `FLOAT` (if not [`no_float`]), `bool`, `char`, `()`, `ImmutableString` | -| `>`, `>=`, `<`, `<=` | | `INT`, `FLOAT` (if not [`no_float`]), `char`, `()`, `ImmutableString` | +| Operators | Assignment operators | Supported for types (see [standard types]) | +| ------------------------- | ---------------------------- | ----------------------------------------------------------------------------- | +| `+`, | `+=` | `INT`, `FLOAT` (if not [`no_float`]), `char`, `ImmutableString` | +| `-`, `*`, `/`, `%`, `~`, | `-=`, `*=`, `/=`, `%=`, `~=` | `INT`, `FLOAT` (if not [`no_float`]) | +| `<<`, `>>` | `<<=`, `>>=` | `INT` | +| `&`, \|, `^` | `&=`, \|=, `^=` | `INT`, `bool` | +| `&&`, \|\| | | `bool` | +| `==`, `!=` | | `INT`, `FLOAT` (if not [`no_float`]), `bool`, `char`, `()`, `ImmutableString` | +| `>`, `>=`, `<`, `<=` | | `INT`, `FLOAT` (if not [`no_float`]), `char`, `()`, `ImmutableString` | diff --git a/doc/src/rust/print-custom.md b/doc/src/rust/print-custom.md index 2aab1b92..2ea6f1e8 100644 --- a/doc/src/rust/print-custom.md +++ b/doc/src/rust/print-custom.md @@ -7,11 +7,11 @@ To use custom types for [`print`] and [`debug`], or convert its value into a [st it is necessary that the following functions be registered (assuming the custom type is `T : Display + Debug`): -| Function | Signature | Typical implementation | Usage | -| ----------- | ------------------------------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------- | -| `to_string` | \|s: &mut T\| -> ImmutableString | `s.to_string().into()` | converts the custom type into a [string] | -| `print` | \|s: &mut T\| -> ImmutableString | `s.to_string().into()` | converts the custom type into a [string] for the [`print`] statement | -| `debug` | \|s: &mut T\| -> ImmutableString | `format!("{:?}", s).into()` | converts the custom type into a [string] for the [`debug`] statement | -| `+` | \|s1: ImmutableString, s: T\| -> ImmutableString | `s1 + s` | appends the custom type to another [string], for `print("Answer: " + type);` usage | -| `+` | \|s: T, s2: ImmutableString\| -> ImmutableString | `s.to_string().push_str(&s2).into();` | appends another [string] to the custom type, for `print(type + " is the answer");` usage | -| `+=` | \|s1: &mut ImmutableString, s: T\| | `s1 += s.to_string()` | appends the custom type to an existing [string], for `s += type;` usage | +| Function | Signature | Typical implementation | Usage | +| ----------- | ---------------------------------------------- | ---------------------------- | -------------------------------------------------------------------- | +| `to_string` | \|x: &mut T\| -> String | `x.to_string()` | converts the custom type into a [string] | +| `print` | \|x: &mut T\| -> String | `x.to_string()` | converts the custom type into a [string] for the [`print`] statement | +| `debug` | \|x: &mut T\| -> String | `format!("{:?}", x)` | converts the custom type into a [string] for the [`debug`] statement | +| `+` | \|s: &str, x: T\| -> String | `format!("{}{}", s, x)` | concatenates the custom type with another [string] | +| `+` | \|x: &mut T, s: &str\| -> String | `x.to_string().push_str(s);` | concatenates another [string] with the custom type | +| `+=` | \|s: &mut ImmutableString, x: T\| | `s += x.to_string()` | appends the custom type to an existing [string] | diff --git a/src/fn_call.rs b/src/fn_call.rs index 7dc97319..832e696c 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -1239,8 +1239,28 @@ pub fn run_builtin_binary_op( use crate::packages::arithmetic::arith_basic::INT::functions::*; let args_type = x.type_id(); + let second_type = y.type_id(); - if y.type_id() != args_type { + if second_type != args_type { + if args_type == TypeId::of::() && second_type == TypeId::of::() { + let x = x.clone().cast::(); + let y = &*y.read_lock::().unwrap(); + + match op { + "+" => return Ok(Some(format!("{}{}", x, y).into())), + _ => (), + } + } else if args_type == TypeId::of::() + && second_type == TypeId::of::() + { + let x = &*x.read_lock::().unwrap(); + let y = y.clone().cast::(); + + match op { + "+" => return Ok(Some((x + y).into())), + _ => (), + } + } return Ok(None); } @@ -1317,6 +1337,7 @@ pub fn run_builtin_binary_op( let y = y.clone().cast::(); match op { + "+" => return Ok(Some(format!("{}{}", x, y).into())), "==" => return Ok(Some((x == y).into())), "!=" => return Ok(Some((x != y).into())), ">" => return Ok(Some((x > y).into())), @@ -1367,8 +1388,19 @@ pub fn run_builtin_op_assignment( use crate::packages::arithmetic::arith_basic::INT::functions::*; let args_type = x.type_id(); + let second_type = y.type_id(); + + if second_type != args_type { + if args_type == TypeId::of::() && second_type == TypeId::of::() { + let y = y.read_lock::().unwrap().deref().clone(); + let mut x = x.write_lock::().unwrap(); + + match op { + "+=" => return Ok(Some(*x += y)), + _ => (), + } + } - if y.type_id() != args_type { return Ok(None); } @@ -1417,6 +1449,14 @@ pub fn run_builtin_op_assignment( "|=" => return Ok(Some(*x = *x || y)), _ => (), } + } else if args_type == TypeId::of::() { + let y = y.read_lock::().unwrap().deref().clone(); + let mut x = x.write_lock::().unwrap(); + + match op { + "+=" => return Ok(Some(*x = format!("{}{}", *x, y).into())), + _ => (), + } } else if args_type == TypeId::of::() { let y = y.read_lock::().unwrap().deref().clone(); let mut x = x.write_lock::().unwrap(); diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index e89e03f8..c799e221 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -43,7 +43,7 @@ macro_rules! reg_functions { } def_package!(crate:MoreStringPackage:"Additional string utilities, including string building.", lib, { - reg_functions!(lib += basic; INT, bool, char, FnPtr); + reg_functions!(lib += basic; INT, bool, FnPtr); #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] @@ -139,15 +139,6 @@ mod string_functions { s } - #[rhai_fn(name = "+=")] - pub fn append_char(s: &mut ImmutableString, ch: char) { - *s += ch; - } - #[rhai_fn(name = "+=")] - pub fn append_string(s: &mut ImmutableString, add: ImmutableString) { - *s += &add; - } - #[rhai_fn(name = "len", get = "len")] pub fn len(s: &str) -> INT { s.chars().count() as INT From 9d93dac8e71e6db35efbb3b32b078feda825e8e7 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 10 Oct 2020 22:14:10 +0800 Subject: [PATCH 2/4] Reserve some more symbols. --- RELEASES.md | 1 + doc/src/appendix/operators.md | 4 ++++ src/token.rs | 20 +++++++++++++++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index e3daa084..ad29d3af 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -15,6 +15,7 @@ Breaking changes * The following `EvalAltResult` variants are removed and merged into `EvalAltResult::ErrorMismatchDataType`: `ErrorCharMismatch`, `ErrorNumericIndexExpr`, `ErrorStringIndexExpr`, `ErrorImportExpr`, `ErrorLogicGuard`, `ErrorBooleanArgMismatch` * `Scope::iter_raw` returns an iterator with an additional field indicating whether the variable is constant or not. * `rhai::ser` and `rhai::de` namespaces are merged into `rhai::serde`. +* New reserved symbols: `++`, `--`, `..`, `...`. New features ------------ diff --git a/doc/src/appendix/operators.md b/doc/src/appendix/operators.md index c0fa4292..7f20f003 100644 --- a/doc/src/appendix/operators.md +++ b/doc/src/appendix/operators.md @@ -56,6 +56,10 @@ Symbols and Patterns | `/*` .. `*/` | comment | block comment | | `(*` .. `*)` | comment | _reserved_ | | `<` .. `>` | angular brackets | _reserved_ | +| `++` | increment | _reserved_ | +| `--` | decrement | _reserved_ | +| `..` | range | _reserved_ | +| `...` | range | _reserved_ | | `#` | hash | _reserved_ | | `@` | at | _reserved_ | | `$` | dollar | _reserved_ | diff --git a/src/token.rs b/src/token.rs index 40555217..181524cc 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1205,6 +1205,10 @@ fn get_next_token_inner( eat_next(stream, pos); return Some((Token::PlusAssign, start_pos)); } + ('+', '+') => { + eat_next(stream, pos); + return Some((Token::Reserved("++".into()), start_pos)); + } ('+', _) if !state.non_unary => return Some((Token::UnaryPlus, start_pos)), ('+', _) => return Some((Token::Plus, start_pos)), @@ -1218,6 +1222,10 @@ fn get_next_token_inner( eat_next(stream, pos); return Some((Token::Reserved("->".into()), start_pos)); } + ('-', '-') => { + eat_next(stream, pos); + return Some((Token::Reserved("--".into()), start_pos)); + } ('-', _) if !state.non_unary => return Some((Token::UnaryMinus, start_pos)), ('-', _) => return Some((Token::Minus, start_pos)), @@ -1282,12 +1290,22 @@ fn get_next_token_inner( (';', _) => return Some((Token::SemiColon, start_pos)), (',', _) => return Some((Token::Comma, start_pos)), + + ('.', '.') => { + eat_next(stream, pos); + + if stream.peek_next() == Some('.') { + eat_next(stream, pos); + return Some((Token::Reserved("...".into()), start_pos)); + } else { + return Some((Token::Reserved("..".into()), start_pos)); + } + } ('.', _) => return Some((Token::Period, start_pos)), ('=', '=') => { eat_next(stream, pos); - // Warn against `===` if stream.peek_next() == Some('=') { eat_next(stream, pos); return Some((Token::Reserved("===".into()), start_pos)); From fd5a932611e72b5b55f46df3b0993b86d527f3f7 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 11 Oct 2020 21:58:11 +0800 Subject: [PATCH 3/4] Implement variable resolver. --- RELEASES.md | 2 + doc/src/SUMMARY.md | 5 +- doc/src/engine/custom-syntax.md | 41 ++-- doc/src/engine/var.md | 75 +++++++ doc/src/links.md | 1 + doc/src/safety/max-operations.md | 1 + doc/src/safety/progress.md | 3 +- src/api.rs | 50 ++++- src/engine.rs | 346 +++++++++++++++++++------------ src/fn_call.rs | 44 ++-- src/fn_native.rs | 18 +- src/lib.rs | 4 +- src/module/mod.rs | 5 +- src/result.rs | 2 +- src/syntax.rs | 86 ++++---- tests/syntax.rs | 23 +- tests/unary_minus.rs | 2 +- tests/var_scope.rs | 40 +++- 18 files changed, 511 insertions(+), 237 deletions(-) create mode 100644 doc/src/engine/var.md diff --git a/RELEASES.md b/RELEASES.md index ad29d3af..c7ce117c 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -16,10 +16,12 @@ Breaking changes * `Scope::iter_raw` returns an iterator with an additional field indicating whether the variable is constant or not. * `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. 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. diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index ce9c1554..e7e02408 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -124,11 +124,12 @@ The Rhai Scripting Language 9. [Advanced Topics](advanced.md) 1. [Capture Scope for Function Call](language/fn-capture.md) 2. [Low-Level API](rust/register-raw.md) - 3. [Use as DSL](engine/dsl.md) + 3. [Variable Resolver](engine/var.md) + 4. [Use as DSL](engine/dsl.md) 1. [Disable Keywords and/or Operators](engine/disable.md) 2. [Custom Operators](engine/custom-op.md) 3. [Extending with Custom Syntax](engine/custom-syntax.md) - 4. [Multiple Instantiation](patterns/multiple.md) + 5. [Multiple Instantiation](patterns/multiple.md) 10. [Appendix](appendix/index.md) 1. [Keywords](appendix/keywords.md) 2. [Operators and Symbols](appendix/operators.md) diff --git a/doc/src/engine/custom-syntax.md b/doc/src/engine/custom-syntax.md index f1d8e481..645c0684 100644 --- a/doc/src/engine/custom-syntax.md +++ b/doc/src/engine/custom-syntax.md @@ -114,13 +114,17 @@ Any custom syntax must include an _implementation_ of it. The function signature of an implementation is: -> `Fn(engine: &Engine, context: &mut EvalContext, scope: &mut Scope, inputs: &[Expression]) -> Result>` +> `Fn(scope: &mut Scope, context: &mut EvalContext, inputs: &[Expression]) -> Result>` where: -* `engine: &Engine` - reference to the current [`Engine`]. -* `context: &mut EvalContext` - mutable reference to the current evaluation _context_; **do not touch**. * `scope: &mut Scope` - mutable reference to the current [`Scope`]; variables can be added to it. + +* `context: &mut EvalContext` - mutable reference to the current evaluation _context_ (**do not touch**) which exposes the following fields: + * `context.engine(): &Engine` - reference to the current [`Engine`]. + * `context.namespace(): &Module` - reference to the current _global namespace_ containing all script-defined functions. + * `context.call_level(): usize` - the current nesting level of function calls. + * `inputs: &[Expression]` - a list of input expression trees. #### WARNING - Lark's Vomit @@ -143,11 +147,12 @@ To access a particular argument, use the following patterns: ### Evaluate an Expression Tree -Use the `engine::eval_expression_tree` method to evaluate an expression tree. +Use the `EvalContext::eval_expression_tree` method to evaluate an arbitrary expression tree +within the current evaluation context. ```rust let expr = inputs.get(0).unwrap(); -let result = engine.eval_expression_tree(context, scope, expr)?; +let result = context.eval_expression_tree(scope, expr)?; ``` ### Declare Variables @@ -157,15 +162,15 @@ New variables maybe declared (usually with a variable name that is passed in via It can simply be pushed into the [`Scope`]. However, beware that all new variables must be declared _prior_ to evaluating any expression tree. -In other words, any `Scope::push` calls must come _before_ any `Engine::eval_expression_tree` calls. +In other words, any `Scope::push` calls must come _before_ any `EvalContext::eval_expression_tree` calls. ```rust let var_name = inputs[0].get_variable_name().unwrap().to_string(); let expr = inputs.get(1).unwrap(); -scope.push(var_name, 0 as INT); // do this BEFORE 'engine.eval_expression_tree'! +scope.push(var_name, 0 as INT); // do this BEFORE 'context.eval_expression_tree'! -let result = engine.eval_expression_tree(context, scope, expr)?; +let result = context.eval_expression_tree(context, scope, expr)?; ``` @@ -182,28 +187,30 @@ The syntax is passed simply as a slice of `&str`. ```rust // Custom syntax implementation fn implementation_func( - engine: &Engine, - context: &mut EvalContext, scope: &mut Scope, + context: &mut EvalContext, inputs: &[Expression] ) -> Result> { let var_name = inputs[0].get_variable_name().unwrap().to_string(); let stmt = inputs.get(1).unwrap(); let condition = inputs.get(2).unwrap(); - // Push one new variable into the 'scope' BEFORE 'eval_expression_tree' + // Push one new variable into the 'scope' BEFORE 'context.eval_expression_tree' scope.push(var_name, 0 as INT); loop { // Evaluate the statement block - engine.eval_expression_tree(context, scope, stmt)?; + context.eval_expression_tree(scope, stmt)?; // Evaluate the condition expression - let stop = !engine.eval_expression_tree(context, scope, condition)? - .as_bool() - .map_err(|_| EvalAltResult::ErrorBooleanArgMismatch( - "do-while".into(), expr.position() - ))?; + let stop = !context.eval_expression_tree(scope, condition)? + .as_bool().map_err(|err| Box::new( + EvalAltResult::ErrorMismatchDataType( + "bool".to_string(), + err.to_string(), + condition.position(), + ) + ))?; if stop { break; diff --git a/doc/src/engine/var.md b/doc/src/engine/var.md new file mode 100644 index 00000000..6e9df16f --- /dev/null +++ b/doc/src/engine/var.md @@ -0,0 +1,75 @@ +Variable Resolver +================= + +{{#include ../links.md}} + +By default, Rhai looks up access to variables from the enclosing block scope, +working its way outwards until it reaches the top (global) level, then it +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: + +```rust +let mut engine = Engine::new(); + +// Register a variable resolver. +engine.on_var(|name, index, engine, scope, lib| { + match name { + "MYSTIC_NUMBER" => Ok(Some((42 as INT).into())), + // Override a variable - make it not found even if it exists! + "DO_NOT_USE" => Err(Box::new( + EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::none()) + )), + // Silently maps 'chameleon' into 'innocent'. + "chameleon" => scope.get_value("innocent").map(Some).ok_or_else(|| Box::new( + EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::none()) + )), + // Return Ok(None) to continue with the normal variable resolution process. + _ => Ok(None) + } +}); +``` + + +Returned Values are Constants +---------------------------- + +Variable values, if any returned, are treated as _constants_ by the script and cannot be assigned to. +This is to avoid needing a mutable reference to the underlying data provider which may not be possible to obtain. + +In order to change these variables, it is best to push them into a custom [`Scope`] instead of using +a variable resolver. Then these variables can be assigned to and their updated values read back after +the script is evaluated. + + +Function Signature +------------------ + +The function signature passed to `Engine::on_var` takes the following form: + +> `Fn(name: &str, index: Option, scope: &Scope, context: &EvalContext) -> Result, Box> + 'static` + +where: + +* `name: &str` - variable name. + +* `index: Option` - an offset from the bottom of the current [`Scope`] that the variable is supposed to reside. + Offsets start from 1, with 1 meaning the last variable in the current [`Scope`]. Essentially the correct variable is at position `scope.len() - index`. + Notice that, if there was an [`eval`] statement before the current statement, new variables may have been introduced and this index may be incorrect. + Therefore, this index is for reference only. It should not be relied upon. + + If `index` is `None`, 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. + +* `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_ 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. + diff --git a/doc/src/links.md b/doc/src/links.md index 54826c66..82bc8739 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -106,6 +106,7 @@ [module]: {{rootUrl}}/rust/modules/index.md [modules]: {{rootUrl}}/rust/modules/index.md [module resolver]: {{rootUrl}}/rust/modules/resolvers.md +[variable resolver]: {{rootUrl}}/engine/var.md [`export`]: {{rootUrl}}/language/modules/export.md [`import`]: {{rootUrl}}/language/modules/import.md diff --git a/doc/src/safety/max-operations.md b/doc/src/safety/max-operations.md index e4778b88..fe42647d 100644 --- a/doc/src/safety/max-operations.md +++ b/doc/src/safety/max-operations.md @@ -3,6 +3,7 @@ Maximum Number of Operations {{#include ../links.md}} + Limit How Long a Script Can Run ------------------------------ diff --git a/doc/src/safety/progress.md b/doc/src/safety/progress.md index 3432a00e..c34eed7c 100644 --- a/doc/src/safety/progress.md +++ b/doc/src/safety/progress.md @@ -7,7 +7,8 @@ It is impossible to know when, or even whether, a script run will end (a.k.a. the [Halting Problem](http://en.wikipedia.org/wiki/Halting_problem)). When dealing with third-party untrusted scripts that may be malicious, to track evaluation progress and -to force-terminate a script prematurely (for any reason), provide a closure to the `Engine::on_progress` method: +to force-terminate a script prematurely (for any reason), provide a closure to the [`Engine`] via +the `Engine::on_progress` method: ```rust let mut engine = Engine::new(); diff --git a/src/api.rs b/src/api.rs index 53a78e95..7b7e6d3d 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,7 +1,7 @@ //! Module that defines the extern API of `Engine`. use crate::any::{Dynamic, Variant}; -use crate::engine::{Engine, Imports, State}; +use crate::engine::{Engine, EvalContext, Imports, State}; use crate::error::ParseError; use crate::fn_native::{IteratorFn, SendSync}; use crate::module::{FuncReturn, Module}; @@ -1686,6 +1686,54 @@ impl Engine { optimize_into_ast(self, scope, stmt, lib, optimization_level) } + /// Provide a callback that will be invoked before each variable access. + /// + /// ## Return Value of Callback + /// + /// Return `Ok(None)` to continue with normal variable access. + /// Return `Ok(Some(Dynamic))` as the variable's value. + /// + /// ## Errors in Callback + /// + /// Return `Err(...)` if there is an error. + /// + /// # Example + /// + /// ``` + /// # fn main() -> Result<(), Box> { + /// use rhai::Engine; + /// + /// let mut engine = Engine::new(); + /// + /// // Register a variable resolver. + /// engine.on_var(|name, _, _, _| { + /// match name { + /// "MYSTIC_NUMBER" => Ok(Some(42_i64.into())), + /// _ => Ok(None) + /// } + /// }); + /// + /// engine.eval::("MYSTIC_NUMBER")?; + /// + /// # Ok(()) + /// # } + /// ``` + #[inline(always)] + pub fn on_var( + &mut self, + callback: impl Fn( + &str, + Option, + &Scope, + &EvalContext, + ) -> Result, Box> + + SendSync + + 'static, + ) -> &mut Self { + self.resolve_var = Some(Box::new(callback)); + self + } + /// Register a callback for script evaluation progress. /// /// # Example diff --git a/src/engine.rs b/src/engine.rs index 42c023e9..9b5f4a53 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -2,7 +2,7 @@ use crate::any::{map_std_type_name, Dynamic, Union}; use crate::fn_call::run_builtin_op_assignment; -use crate::fn_native::{Callback, FnPtr}; +use crate::fn_native::{Callback, FnPtr, OnVarCallback}; use crate::module::{Module, ModuleRef}; use crate::optimize::OptimizationLevel; use crate::packages::{Package, PackagesCollection, StandardPackage}; @@ -10,7 +10,7 @@ use crate::parser::{Expr, ReturnType, Stmt, INT}; use crate::r#unsafe::unsafe_cast_var_name_to_lifetime; use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; -use crate::syntax::{CustomSyntax, EvalContext}; +use crate::syntax::CustomSyntax; use crate::token::Position; use crate::{calc_fn_hash, StaticVec}; @@ -148,7 +148,7 @@ pub enum Target<'a> { } #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] -impl Target<'_> { +impl<'a> Target<'a> { /// Is the `Target` a reference pointing to other data? #[allow(dead_code)] #[inline(always)] @@ -207,7 +207,7 @@ impl Target<'_> { } /// Get the value of the `Target` as a `Dynamic`, cloning a referenced value if necessary. #[inline(always)] - pub fn clone_into_dynamic(self) -> Dynamic { + pub fn take_or_clone(self) -> Dynamic { match self { Self::Ref(r) => r.clone(), // Referenced value is cloned #[cfg(not(feature = "no_closure"))] @@ -218,6 +218,14 @@ impl Target<'_> { Self::StringChar(_, _, ch) => ch, // Character is taken } } + /// Take a `&mut Dynamic` reference from the `Target`. + #[inline(always)] + pub fn take_ref(self) -> Option<&'a mut Dynamic> { + match self { + Self::Ref(r) => Some(r), + _ => None, + } + } /// Get a mutable reference from the `Target`. #[inline(always)] pub fn as_mut(&mut self) -> &mut Dynamic { @@ -374,6 +382,39 @@ pub struct Limits { pub max_map_size: usize, } +/// Context of a script evaluation process. +#[derive(Debug)] +pub struct EvalContext<'e, 'a, 's, 'm, 't, 'd: 't> { + pub(crate) engine: &'e Engine, + pub(crate) mods: &'a mut Imports, + pub(crate) state: &'s mut State, + pub(crate) lib: &'m Module, + pub(crate) this_ptr: &'t mut Option<&'d mut Dynamic>, + pub(crate) level: usize, +} + +impl<'e, 'a, 's, 'm, 't, 'd> EvalContext<'e, 'a, 's, 'm, 't, 'd> { + /// The current `Engine`. + pub fn engine(&self) -> &'e Engine { + self.engine + } + /// _[INTERNALS]_ The current set of modules imported via `import` statements. + /// Available under the `internals` feature only. + #[cfg(feature = "internals")] + #[cfg(not(feature = "no_modules"))] + pub fn imports(&self) -> &'a Imports { + self.mods + } + /// The global namespace containing definition of all script-defined functions. + pub fn namespace(&self) -> &'m Module { + self.lib + } + /// The current nesting level of function calls. + pub fn call_level(&self) -> usize { + self.level + } +} + /// Rhai main scripting engine. /// /// ``` @@ -412,6 +453,8 @@ pub struct Engine { pub(crate) custom_keywords: Option>, /// Custom syntax. pub(crate) custom_syntax: Option>, + /// Callback closure for resolving variable access. + pub(crate) resolve_var: Option, /// Callback closure for implementing the `print` command. pub(crate) print: Callback, @@ -522,88 +565,6 @@ pub fn search_imports_mut<'s>( }) } -/// Search for a variable within the scope or within imports, -/// depending on whether the variable name is qualified. -pub fn search_namespace<'s, 'a>( - scope: &'s mut Scope, - mods: &'s mut Imports, - state: &mut State, - this_ptr: &'s mut Option<&mut Dynamic>, - expr: &'a Expr, -) -> Result<(&'s mut Dynamic, &'a str, ScopeEntryType, Position), Box> { - match expr { - Expr::Variable(v) => match v.as_ref() { - // 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.new_position(*pos), - })?; - - // Module variables are constant - Ok((target, name, ScopeEntryType::Constant, *pos)) - } - // Normal variable access - _ => search_scope_only(scope, state, this_ptr, expr), - }, - _ => unreachable!(), - } -} - -/// Search for a variable within the scope -pub fn search_scope_only<'s, 'a>( - scope: &'s mut Scope, - state: &mut State, - this_ptr: &'s mut Option<&mut Dynamic>, - expr: &'a Expr, -) -> Result<(&'s mut Dynamic, &'a str, ScopeEntryType, Position), Box> { - let ((name, pos), _, _, index) = match expr { - Expr::Variable(v) => v.as_ref(), - _ => unreachable!(), - }; - - // Check if the variable is `this` - if name == KEYWORD_THIS { - if let Some(val) = this_ptr { - return Ok(((*val).into(), KEYWORD_THIS, ScopeEntryType::Normal, *pos)); - } else { - return EvalAltResult::ErrorUnboundThis(*pos).into(); - } - } - - // Check if it is directly indexed - let index = if state.always_search { None } else { *index }; - - let index = if let Some(index) = index { - scope.len() - index.get() - } else { - // Find the variable in the scope - scope - .get_index(name) - .ok_or_else(|| EvalAltResult::ErrorVariableNotFound(name.into(), *pos))? - .0 - }; - - let (val, typ) = scope.get_mut(index); - - // Check for data race - probably not necessary because the only place it should conflict is in a method call - // when the object variable is also used as a parameter. - // if cfg!(not(feature = "no_closure")) && val.is_locked() { - // return EvalAltResult::ErrorDataRace(name.into(), *pos).into(); - // } - - Ok((val, name, typ, *pos)) -} - impl Engine { /// Create a new `Engine` #[inline(always)] @@ -628,6 +589,9 @@ impl Engine { custom_keywords: None, custom_syntax: None, + // variable resolver + resolve_var: None, + // default print/debug implementations print: Box::new(default_print), debug: Box::new(default_print), @@ -678,6 +642,8 @@ impl Engine { custom_keywords: None, custom_syntax: None, + resolve_var: None, + print: Box::new(|_| {}), debug: Box::new(|_| {}), progress: None, @@ -702,6 +668,111 @@ impl Engine { } } + /// Search for a variable within the scope or within imports, + /// depending on whether the variable name is qualified. + pub(crate) fn search_namespace<'s, 'a>( + &self, + scope: &'s mut Scope, + mods: &'s mut Imports, + state: &mut State, + lib: &Module, + this_ptr: &'s mut Option<&mut Dynamic>, + expr: &'a Expr, + ) -> Result<(Target<'s>, &'a str, ScopeEntryType, Position), Box> { + match expr { + Expr::Variable(v) => match v.as_ref() { + // 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), + })?; + + // Module variables are constant + Ok((target.into(), name, ScopeEntryType::Constant, *pos)) + } + // Normal variable access + _ => self.search_scope_only(scope, mods, state, lib, this_ptr, expr), + }, + _ => unreachable!(), + } + } + + /// Search for a variable within the scope + pub(crate) fn search_scope_only<'s, 'a>( + &self, + scope: &'s mut Scope, + mods: &mut Imports, + state: &mut State, + lib: &Module, + this_ptr: &'s mut Option<&mut Dynamic>, + expr: &'a Expr, + ) -> Result<(Target<'s>, &'a str, ScopeEntryType, Position), Box> { + let ((name, pos), _, _, index) = match expr { + Expr::Variable(v) => v.as_ref(), + _ => unreachable!(), + }; + + // Check if the variable is `this` + if name == KEYWORD_THIS { + if let Some(val) = this_ptr { + return Ok(((*val).into(), KEYWORD_THIS, ScopeEntryType::Normal, *pos)); + } else { + return EvalAltResult::ErrorUnboundThis(*pos).into(); + } + } + + // Check if it is directly indexed + let index = if state.always_search { None } else { *index }; + + // Check the variable resolver, if any + if let Some(ref resolve_var) = self.resolve_var { + let context = EvalContext { + engine: self, + mods, + state, + lib, + this_ptr, + level: 0, + }; + if let Some(result) = resolve_var(name, index.map(|v| v.get()), 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() + } else { + // Find the variable in the scope + scope + .get_index(name) + .ok_or_else(|| EvalAltResult::ErrorVariableNotFound(name.into(), *pos))? + .0 + }; + + let (val, typ) = scope.get_mut(index); + + // Check for data race - probably not necessary because the only place it should conflict is in a method call + // when the object variable is also used as a parameter. + // if cfg!(not(feature = "no_closure")) && val.is_locked() { + // return EvalAltResult::ErrorDataRace(name.into(), *pos).into(); + // } + + Ok((val.into(), name, typ, *pos)) + } + /// Chain-evaluate a dot/index chain. /// Position in `EvalAltResult` is `None` and must be set afterwards. #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] @@ -750,7 +821,7 @@ impl Engine { state, lib, this_ptr, obj_ptr, expr, idx_values, next_chain, level, new_val, ) - .map_err(|err| err.new_position(*pos)) + .map_err(|err| err.fill_position(*pos)) } // xxx[rhs] = new_val _ if new_val.is_some() => { @@ -800,7 +871,7 @@ impl Engine { // xxx[rhs] _ => self .get_indexed_mut(state, lib, target, idx_val, pos, false, true, level) - .map(|v| (v.clone_into_dynamic(), false)), + .map(|v| (v.take_or_clone(), false)), } } @@ -815,7 +886,7 @@ impl Engine { state, lib, name, *hash, target, idx_val, &def_val, *native, false, level, ) - .map_err(|err| err.new_position(*pos)) + .map_err(|err| err.fill_position(*pos)) } // xxx.module::fn_name(...) - syntax error Expr::FnCall(_) => unreachable!(), @@ -837,7 +908,7 @@ impl Engine { state, lib, target, index, *pos, false, false, level, )?; - Ok((val.clone_into_dynamic(), false)) + Ok((val.take_or_clone(), false)) } // xxx.id = ??? Expr::Property(x) if new_val.is_some() => { @@ -849,7 +920,7 @@ impl Engine { level, ) .map(|(v, _)| (v, true)) - .map_err(|err| err.new_position(*pos)) + .map_err(|err| err.fill_position(*pos)) } // xxx.id Expr::Property(x) => { @@ -860,7 +931,7 @@ impl Engine { level, ) .map(|(v, _)| (v, false)) - .map_err(|err| err.new_position(*pos)) + .map_err(|err| err.fill_position(*pos)) } // {xxx:map}.sub_lhs[expr] | {xxx:map}.sub_lhs.expr Expr::Index(x) | Expr::Dot(x) if target.is::() => { @@ -883,7 +954,7 @@ impl Engine { state, lib, name, *hash, target, idx_val, &def_val, *native, false, level, ) - .map_err(|err| err.new_position(*pos))?; + .map_err(|err| err.fill_position(*pos))?; val.into() } // {xxx:map}.module::fn_name(...) - syntax error @@ -896,7 +967,7 @@ impl Engine { state, lib, this_ptr, &mut val, expr, idx_values, next_chain, level, new_val, ) - .map_err(|err| err.new_position(*pos)) + .map_err(|err| err.fill_position(*pos)) } // xxx.sub_lhs[expr] | xxx.sub_lhs.expr Expr::Index(x) | Expr::Dot(x) => { @@ -913,7 +984,7 @@ impl Engine { state, lib, getter, 0, args, is_ref, true, false, None, &None, level, ) - .map_err(|err| err.new_position(*pos))?; + .map_err(|err| err.fill_position(*pos))?; let val = &mut val; @@ -929,7 +1000,7 @@ impl Engine { level, new_val, ) - .map_err(|err| err.new_position(*pos))?; + .map_err(|err| err.fill_position(*pos))?; // Feed the value back via a setter just in case it has been updated if updated || may_be_changed { @@ -945,7 +1016,7 @@ impl Engine { EvalAltResult::ErrorDotExpr(_, _) => { Ok(Default::default()) } - _ => Err(err.new_position(*pos)), + _ => Err(err.fill_position(*pos)), }, )?; } @@ -961,7 +1032,7 @@ impl Engine { state, lib, name, *hash, target, idx_val, &def_val, *native, false, level, ) - .map_err(|err| err.new_position(*pos))?; + .map_err(|err| err.fill_position(*pos))?; let val = &mut val; let target = &mut val.into(); @@ -969,7 +1040,7 @@ impl Engine { state, lib, this_ptr, target, expr, idx_values, next_chain, level, new_val, ) - .map_err(|err| err.new_position(*pos)) + .map_err(|err| err.fill_position(*pos)) } // xxx.module::fn_name(...) - syntax error Expr::FnCall(_) => unreachable!(), @@ -1017,10 +1088,10 @@ impl Engine { let (var_name, var_pos) = &x.0; self.inc_operations(state) - .map_err(|err| err.new_position(*var_pos))?; + .map_err(|err| err.fill_position(*var_pos))?; let (target, _, typ, pos) = - search_namespace(scope, mods, state, this_ptr, dot_lhs)?; + self.search_namespace(scope, mods, state, lib, this_ptr, dot_lhs)?; // Constants cannot be modified match typ { @@ -1036,7 +1107,7 @@ impl Engine { state, lib, &mut None, obj_ptr, dot_rhs, idx_values, chain_type, level, new_val, ) .map(|(v, _)| v) - .map_err(|err| err.new_position(*op_pos)) + .map_err(|err| err.fill_position(*op_pos)) } // {expr}.??? = ??? or {expr}[???] = ??? expr if new_val.is_some() => { @@ -1050,7 +1121,7 @@ impl Engine { state, lib, this_ptr, obj_ptr, dot_rhs, idx_values, chain_type, level, new_val, ) .map(|(v, _)| v) - .map_err(|err| err.new_position(*op_pos)) + .map_err(|err| err.fill_position(*op_pos)) } } } @@ -1075,7 +1146,7 @@ impl Engine { level: usize, ) -> Result<(), Box> { self.inc_operations(state) - .map_err(|err| err.new_position(expr.position()))?; + .map_err(|err| err.fill_position(expr.position()))?; match expr { Expr::FnCall(x) if x.1.is_none() => { @@ -1248,7 +1319,7 @@ impl Engine { level: usize, ) -> Result> { self.inc_operations(state) - .map_err(|err| err.new_position(rhs.position()))?; + .map_err(|err| err.fill_position(rhs.position()))?; let lhs_value = self.eval_expr(scope, mods, state, lib, this_ptr, lhs, level)?; let rhs_value = self.eval_expr(scope, mods, state, lib, this_ptr, rhs, level)?; @@ -1270,7 +1341,7 @@ impl Engine { if self .call_native_fn(state, lib, op, hash, args, false, false, &def_value) - .map_err(|err| err.new_position(rhs.position()))? + .map_err(|err| err.fill_position(rhs.position()))? .0 .as_bool() .unwrap_or(false) @@ -1310,7 +1381,7 @@ impl Engine { level: usize, ) -> Result> { self.inc_operations(state) - .map_err(|err| err.new_position(expr.position()))?; + .map_err(|err| err.fill_position(expr.position()))?; let result = match expr { Expr::Expr(x) => self.eval_expr(scope, mods, state, lib, this_ptr, x.as_ref(), level), @@ -1329,8 +1400,9 @@ impl Engine { } } Expr::Variable(_) => { - let (val, _, _, _) = search_namespace(scope, mods, state, this_ptr, expr)?; - Ok(val.clone()) + let (val, _, _, _) = + self.search_namespace(scope, mods, state, lib, this_ptr, expr)?; + Ok(val.take_or_clone()) } Expr::Property(_) => unreachable!(), @@ -1340,12 +1412,18 @@ impl Engine { // var op= rhs Expr::Assignment(x) if matches!(x.0, Expr::Variable(_)) => { let (lhs_expr, op, rhs_expr, op_pos) = x.as_ref(); - let mut rhs_val = - self.eval_expr(scope, mods, state, lib, this_ptr, rhs_expr, level)?; - let (lhs_ptr, name, typ, pos) = - search_namespace(scope, mods, state, this_ptr, lhs_expr)?; + let mut rhs_val = self + .eval_expr(scope, mods, state, lib, this_ptr, rhs_expr, level)? + .flatten(); + let (mut lhs_ptr, name, typ, pos) = + self.search_namespace(scope, mods, state, lib, this_ptr, lhs_expr)?; + + if !lhs_ptr.is_ref() { + return EvalAltResult::ErrorAssignmentToConstant(name.to_string(), pos).into(); + } + self.inc_operations(state) - .map_err(|err| err.new_position(pos))?; + .map_err(|err| err.fill_position(pos))?; match typ { // Assignment to constant variable @@ -1354,11 +1432,10 @@ impl Engine { )), // Normal assignment ScopeEntryType::Normal if op.is_empty() => { - let value = rhs_val.flatten(); if cfg!(not(feature = "no_closure")) && lhs_ptr.is_shared() { - *lhs_ptr.write_lock::().unwrap() = value; + *lhs_ptr.as_mut().write_lock::().unwrap() = rhs_val; } else { - *lhs_ptr = value; + *lhs_ptr.as_mut() = rhs_val; } Ok(Default::default()) } @@ -1369,7 +1446,8 @@ impl Engine { // 3) Map to `var = var op rhs` // Qualifiers (none) + function name + number of arguments + argument `TypeId`'s. - let arg_types = once(lhs_ptr.type_id()).chain(once(rhs_val.type_id())); + let arg_types = + once(lhs_ptr.as_mut().type_id()).chain(once(rhs_val.type_id())); let hash_fn = calc_fn_hash(empty(), op, 2, arg_types); match self @@ -1383,10 +1461,10 @@ impl Engine { let lhs_ptr_inner; if cfg!(not(feature = "no_closure")) && lhs_ptr.is_shared() { - lock_guard = lhs_ptr.write_lock::().unwrap(); + lock_guard = lhs_ptr.as_mut().write_lock::().unwrap(); lhs_ptr_inner = lock_guard.deref_mut(); } else { - lhs_ptr_inner = lhs_ptr; + lhs_ptr_inner = lhs_ptr.as_mut(); } let args = &mut [lhs_ptr_inner, &mut rhs_val]; @@ -1399,13 +1477,14 @@ impl Engine { } } // Built-in op-assignment function - _ if run_builtin_op_assignment(op, lhs_ptr, &rhs_val)?.is_some() => {} + _ if run_builtin_op_assignment(op, lhs_ptr.as_mut(), &rhs_val)? + .is_some() => {} // Not built-in: expand to `var = var op rhs` _ => { let op = &op[..op.len() - 1]; // extract operator without = // Clone the LHS value - let args = &mut [&mut lhs_ptr.clone(), &mut rhs_val]; + let args = &mut [&mut lhs_ptr.as_mut().clone(), &mut rhs_val]; // Run function let (value, _) = self @@ -1413,14 +1492,14 @@ impl Engine { state, lib, op, 0, args, false, false, false, None, &None, level, ) - .map_err(|err| err.new_position(*op_pos))?; + .map_err(|err| err.fill_position(*op_pos))?; let value = value.flatten(); if cfg!(not(feature = "no_closure")) && lhs_ptr.is_shared() { - *lhs_ptr.write_lock::().unwrap() = value; + *lhs_ptr.as_mut().write_lock::().unwrap() = value; } else { - *lhs_ptr = value; + *lhs_ptr.as_mut() = value; } } } @@ -1451,7 +1530,7 @@ impl Engine { state, lib, op, 0, args, false, false, false, None, &None, level, ) .map(|(v, _)| v) - .map_err(|err| err.new_position(*op_pos))?; + .map_err(|err| err.fill_position(*op_pos))?; Some((result, rhs_expr.position())) }; @@ -1520,7 +1599,7 @@ impl Engine { scope, mods, state, lib, this_ptr, name, args_expr, &def_val, *hash, *native, false, *capture, level, ) - .map_err(|err| err.new_position(*pos)) + .map_err(|err| err.fill_position(*pos)) } // Module-qualified function call @@ -1530,7 +1609,7 @@ impl Engine { scope, mods, state, lib, this_ptr, modules, name, args_expr, *def_val, *hash, *capture, level, ) - .map_err(|err| err.new_position(*pos)) + .map_err(|err| err.fill_position(*pos)) } Expr::In(x) => self.eval_in_expr(scope, mods, state, lib, this_ptr, &x.0, &x.1, level), @@ -1571,20 +1650,21 @@ impl Engine { let func = (x.0).1.as_ref(); let ep = (x.0).0.iter().map(|e| e.into()).collect::>(); let mut context = EvalContext { + engine: self, mods, state, lib, this_ptr, level, }; - func(self, &mut context, scope, ep.as_ref()) + func(scope, &mut context, ep.as_ref()) } _ => unreachable!(), }; self.check_data_size(result) - .map_err(|err| err.new_position(expr.position())) + .map_err(|err| err.fill_position(expr.position())) } /// Evaluate a statement @@ -1605,7 +1685,7 @@ impl Engine { level: usize, ) -> Result> { self.inc_operations(state) - .map_err(|err| err.new_position(stmt.position()))?; + .map_err(|err| err.fill_position(stmt.position()))?; let result = match stmt { // No-op @@ -1720,7 +1800,7 @@ impl Engine { } self.inc_operations(state) - .map_err(|err| err.new_position(stmt.position()))?; + .map_err(|err| err.fill_position(stmt.position()))?; match self.eval_stmt(scope, mods, state, lib, this_ptr, stmt, level) { Ok(_) => (), @@ -1872,7 +1952,7 @@ impl Engine { }; self.check_data_size(result) - .map_err(|err| err.new_position(stmt.position())) + .map_err(|err| err.fill_position(stmt.position())) } /// Check a result to ensure that the data size is within allowable limit. diff --git a/src/fn_call.rs b/src/fn_call.rs index 832e696c..aa35b527 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -2,9 +2,9 @@ use crate::any::Dynamic; use crate::engine::{ - search_imports, search_namespace, search_scope_only, Engine, Imports, State, KEYWORD_DEBUG, - KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CALL, KEYWORD_FN_PTR_CURRY, KEYWORD_IS_DEF_FN, - KEYWORD_IS_DEF_VAR, KEYWORD_PRINT, KEYWORD_TYPE_OF, + search_imports, Engine, Imports, State, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, + KEYWORD_FN_PTR_CALL, KEYWORD_FN_PTR_CURRY, KEYWORD_IS_DEF_FN, KEYWORD_IS_DEF_VAR, + KEYWORD_PRINT, KEYWORD_TYPE_OF, }; use crate::error::ParseErrorType; use crate::fn_native::{FnCallArgs, FnPtr}; @@ -861,7 +861,7 @@ impl Engine { }) .and_then(|s| FnPtr::try_from(s)) .map(Into::::into) - .map_err(|err| err.new_position(expr.position())); + .map_err(|err| err.fill_position(expr.position())); } } @@ -1000,7 +1000,7 @@ impl Engine { })?; let result = if !script.is_empty() { self.eval_script_expr(scope, mods, state, lib, script, level + 1) - .map_err(|err| err.new_position(expr.position())) + .map_err(|err| err.fill_position(expr.position())) } else { Ok(().into()) }; @@ -1040,18 +1040,21 @@ impl Engine { .map(|expr| self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)) .collect::>()?; - let (target, _, _, pos) = search_namespace(scope, mods, state, this_ptr, lhs)?; + let (target, _, _, pos) = + self.search_namespace(scope, mods, state, lib, this_ptr, lhs)?; self.inc_operations(state) - .map_err(|err| err.new_position(pos))?; + .map_err(|err| err.fill_position(pos))?; - args = if target.is_shared() { - arg_values.insert(0, target.flatten_clone()); + args = if target.is_shared() || target.is_value() { + arg_values.insert(0, target.take_or_clone().flatten()); arg_values.iter_mut().collect() } else { - // Turn it into a method call only if the object is not shared + // Turn it into a method call only if the object is not shared and not a simple value is_ref = true; - once(target).chain(arg_values.iter_mut()).collect() + once(target.take_ref().unwrap()) + .chain(arg_values.iter_mut()) + .collect() }; } // func(..., ...) @@ -1121,16 +1124,23 @@ impl Engine { .collect::>()?; // Get target reference to first argument + let var_expr = args_expr.get(0).unwrap(); let (target, _, _, pos) = - search_scope_only(scope, state, this_ptr, args_expr.get(0).unwrap())?; + self.search_scope_only(scope, mods, state, lib, this_ptr, var_expr)?; self.inc_operations(state) - .map_err(|err| err.new_position(pos))?; + .map_err(|err| err.fill_position(pos))?; - let (first, rest) = arg_values.split_first_mut().unwrap(); - first_arg_value = Some(first); - - args = once(target).chain(rest.iter_mut()).collect(); + if target.is_shared() || target.is_value() { + arg_values[0] = target.take_or_clone().flatten(); + args = arg_values.iter_mut().collect(); + } else { + let (first, rest) = arg_values.split_first_mut().unwrap(); + first_arg_value = Some(first); + args = once(target.take_ref().unwrap()) + .chain(rest.iter_mut()) + .collect(); + } } // func(..., ...) or func(mod::x, ...) _ => { diff --git a/src/fn_native.rs b/src/fn_native.rs index 01c1f17b..8cef4998 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -1,11 +1,12 @@ //! Module defining interfaces to native-Rust functions. use crate::any::Dynamic; -use crate::engine::Engine; +use crate::engine::{Engine, EvalContext}; use crate::module::Module; use crate::parser::{FnAccess, ScriptFnDef}; use crate::plugin::PluginFunction; use crate::result::EvalAltResult; +use crate::scope::Scope; use crate::token::{is_valid_identifier, Position}; use crate::utils::ImmutableString; @@ -220,6 +221,21 @@ pub type Callback = Box R + 'static>; #[cfg(feature = "sync")] pub type Callback = Box R + Send + Sync + 'static>; +/// A standard callback function. +#[cfg(not(feature = "sync"))] +pub type OnVarCallback = Box< + dyn Fn(&str, Option, &Scope, &EvalContext) -> Result, Box> + + 'static, +>; +/// A standard callback function. +#[cfg(feature = "sync")] +pub type OnVarCallback = Box< + dyn Fn(&str, Option, &Scope, &EvalContext) -> Result, Box> + + Send + + Sync + + 'static, +>; + /// A type encapsulating a function callable by Rhai. #[derive(Clone)] pub enum CallableFunction { diff --git a/src/lib.rs b/src/lib.rs index 2cb73e0d..b024ca69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,7 +83,7 @@ mod r#unsafe; mod utils; pub use any::Dynamic; -pub use engine::Engine; +pub use engine::{Engine, EvalContext}; pub use error::{ParseError, ParseErrorType}; pub use fn_native::{FnPtr, IteratorFn}; pub use fn_register::{RegisterFn, RegisterResultFn}; @@ -91,7 +91,7 @@ pub use module::Module; pub use parser::{ImmutableString, AST, INT}; pub use result::EvalAltResult; pub use scope::Scope; -pub use syntax::{EvalContext, Expression}; +pub use syntax::Expression; pub use token::Position; #[cfg(feature = "internals")] diff --git a/src/module/mod.rs b/src/module/mod.rs index 3056d19b..fa509656 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -1430,11 +1430,12 @@ impl Module { ) .map_err(|err| { // Wrap the error in a module-error - Box::new(EvalAltResult::ErrorInModule( + EvalAltResult::ErrorInModule( "".to_string(), err, Position::none(), - )) + ) + .into() }) }, ); diff --git a/src/result.rs b/src/result.rs index b459f6f5..b5191cb4 100644 --- a/src/result.rs +++ b/src/result.rs @@ -339,7 +339,7 @@ impl EvalAltResult { /// Consume the current `EvalAltResult` and return a new one with the specified `Position` /// if the current position is `Position::None`. #[inline(always)] - pub(crate) fn new_position(mut self: Box, new_position: Position) -> Box { + pub(crate) fn fill_position(mut self: Box, new_position: Position) -> Box { if self.position().is_none() { self.set_position(new_position); } diff --git a/src/syntax.rs b/src/syntax.rs index 35c6b00e..f0372649 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -1,10 +1,9 @@ //! Module implementing custom syntax for `Engine`. use crate::any::Dynamic; -use crate::engine::{Engine, Imports, State, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT}; +use crate::engine::{Engine, EvalContext, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT}; use crate::error::{LexError, ParseError}; use crate::fn_native::{SendSync, Shared}; -use crate::module::Module; use crate::parser::Expr; use crate::result::EvalAltResult; use crate::scope::Scope; @@ -19,15 +18,11 @@ use crate::stdlib::{ /// A general expression evaluation trait object. #[cfg(not(feature = "sync"))] -pub type FnCustomSyntaxEval = dyn Fn( - &Engine, - &mut EvalContext, - &mut Scope, - &[Expression], -) -> Result>; +pub type FnCustomSyntaxEval = + dyn Fn(&mut Scope, &mut EvalContext, &[Expression]) -> Result>; /// A general expression evaluation trait object. #[cfg(feature = "sync")] -pub type FnCustomSyntaxEval = dyn Fn(&Engine, &mut EvalContext, &mut Scope, &[Expression]) -> Result> +pub type FnCustomSyntaxEval = dyn Fn(&mut Scope, &mut EvalContext, &[Expression]) -> Result> + Send + Sync; @@ -63,6 +58,30 @@ impl Expression<'_> { } } +impl EvalContext<'_, '_, '_, '_, '_, '_> { + /// Evaluate an expression tree. + /// + /// ## WARNING - Low Level API + /// + /// This function is very low level. It evaluates an expression from an AST. + #[inline(always)] + pub fn eval_expression_tree( + &mut self, + scope: &mut Scope, + expr: &Expression, + ) -> Result> { + self.engine.eval_expr( + scope, + self.mods, + self.state, + self.lib, + self.this_ptr, + expr.expr(), + self.level, + ) + } +} + #[derive(Clone)] pub struct CustomSyntax { pub segments: StaticVec, @@ -77,27 +96,17 @@ impl fmt::Debug for CustomSyntax { } } -/// Context of a script evaluation process. -#[derive(Debug)] -pub struct EvalContext<'a, 's, 'm, 't, 'd: 't> { - pub(crate) mods: &'a mut Imports, - pub(crate) state: &'s mut State, - pub(crate) lib: &'m Module, - pub(crate) this_ptr: &'t mut Option<&'d mut Dynamic>, - pub(crate) level: usize, -} - impl Engine { + /// Register a custom syntax with the `Engine`. + /// + /// * `keywords` holds a slice of strings that define the custom syntax. + /// * `new_vars` is the number of new variables declared by this custom syntax, or the number of variables removed (if negative). + /// * `func` is the implementation function. pub fn register_custom_syntax + ToString>( &mut self, keywords: &[S], - scope_delta: isize, - func: impl Fn( - &Engine, - &mut EvalContext, - &mut Scope, - &[Expression], - ) -> Result> + new_vars: isize, + func: impl Fn(&mut Scope, &mut EvalContext, &[Expression]) -> Result> + SendSync + 'static, ) -> Result<&mut Self, ParseError> { @@ -176,7 +185,7 @@ impl Engine { let syntax = CustomSyntax { segments, func: (Box::new(func) as Box).into(), - scope_delta, + scope_delta: new_vars, }; if self.custom_syntax.is_none() { @@ -190,27 +199,4 @@ impl Engine { Ok(self) } - - /// Evaluate an expression tree. - /// - /// ## WARNING - Low Level API - /// - /// This function is very low level. It evaluates an expression from an AST. - #[inline(always)] - pub fn eval_expression_tree( - &self, - context: &mut EvalContext, - scope: &mut Scope, - expr: &Expression, - ) -> Result> { - self.eval_expr( - scope, - context.mods, - context.state, - context.lib, - context.this_ptr, - expr.expr(), - context.level, - ) - } } diff --git a/tests/syntax.rs b/tests/syntax.rs index 0f0090f9..f9c5acda 100644 --- a/tests/syntax.rs +++ b/tests/syntax.rs @@ -22,21 +22,28 @@ fn test_custom_syntax() -> Result<(), Box> { "do", "|", "$ident$", "|", "->", "$block$", "while", "$expr$", ], 1, - |engine: &Engine, context: &mut EvalContext, scope: &mut Scope, inputs: &[Expression]| { + |scope: &mut Scope, context: &mut EvalContext, inputs: &[Expression]| { let var_name = inputs[0].get_variable_name().unwrap().to_string(); let stmt = inputs.get(1).unwrap(); - let expr = inputs.get(2).unwrap(); + let condition = inputs.get(2).unwrap(); scope.push(var_name, 0 as INT); loop { - engine.eval_expression_tree(context, scope, stmt)?; + context.eval_expression_tree(scope, stmt)?; - if !engine - .eval_expression_tree(context, scope, expr)? + let stop = !context + .eval_expression_tree(scope, condition)? .as_bool() - .map_err(|err| engine.make_type_mismatch_err::(err, expr.position()))? - { + .map_err(|err| { + Box::new(EvalAltResult::ErrorMismatchDataType( + "bool".to_string(), + err.to_string(), + condition.position(), + )) + })?; + + if stop { break; } } @@ -61,7 +68,7 @@ fn test_custom_syntax() -> Result<(), Box> { // The first symbol must be an identifier assert_eq!( *engine - .register_custom_syntax(&["!"], 0, |_, _, _, _| Ok(().into())) + .register_custom_syntax(&["!"], 0, |_, _, _| Ok(().into())) .expect_err("should error") .0, ParseErrorType::BadInput("Improper symbol for custom syntax: '!'".to_string()) diff --git a/tests/unary_minus.rs b/tests/unary_minus.rs index 87e2aa2b..5aebe8da 100644 --- a/tests/unary_minus.rs +++ b/tests/unary_minus.rs @@ -9,7 +9,7 @@ fn test_unary_minus() -> Result<(), Box> { #[cfg(not(feature = "no_function"))] assert_eq!(engine.eval::("fn neg(x) { -x } neg(5)")?, -5); - assert_eq!(engine.eval::("5 - -+++--+-5")?, 0); + assert_eq!(engine.eval::("5 - -+ + + - -+-5")?, 0); Ok(()) } diff --git a/tests/var_scope.rs b/tests/var_scope.rs index 0b3cc1b9..ede4ef1d 100644 --- a/tests/var_scope.rs +++ b/tests/var_scope.rs @@ -1,4 +1,4 @@ -use rhai::{Engine, EvalAltResult, Scope, INT}; +use rhai::{Engine, EvalAltResult, Position, Scope, INT}; #[test] fn test_var_scope() -> Result<(), Box> { @@ -52,3 +52,41 @@ fn test_scope_eval() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_var_resolver() -> Result<(), Box> { + let mut engine = Engine::new(); + + let mut scope = Scope::new(); + scope.push("innocent", 1 as INT); + scope.push("chameleon", 123 as INT); + scope.push("DO_NOT_USE", 999 as INT); + + engine.on_var(|name, _, scope, _| { + match name { + "MYSTIC_NUMBER" => Ok(Some((42 as INT).into())), + // Override a variable - make it not found even if it exists! + "DO_NOT_USE" => { + Err(EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::none()).into()) + } + // Silently maps 'chameleon' into 'innocent'. + "chameleon" => scope.get_value("innocent").map(Some).ok_or_else(|| { + EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::none()).into() + }), + // Return Ok(None) to continue with the normal variable resolution process. + _ => Ok(None), + } + }); + + assert_eq!( + engine.eval_with_scope::(&mut scope, "MYSTIC_NUMBER")?, + 42 + ); + assert_eq!(engine.eval_with_scope::(&mut scope, "chameleon")?, 1); + assert!( + matches!(*engine.eval_with_scope::(&mut scope, "DO_NOT_USE").expect_err("should error"), + EvalAltResult::ErrorVariableNotFound(n, _) if n == "DO_NOT_USE") + ); + + Ok(()) +} From e343bcfa8faae7638e33e67aa606e6bbc2df56b3 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 11 Oct 2020 22:41:26 +0800 Subject: [PATCH 4/4] Change Option to usize for variable resolver index. --- doc/src/SUMMARY.md | 1 + doc/src/advanced.md | 12 +------ doc/src/engine/custom-syntax.md | 2 +- doc/src/engine/var.md | 13 +++---- doc/src/patterns/dynamic-const.md | 56 +++++++++++++++++++++++++++++++ src/api.rs | 7 +--- src/engine.rs | 2 +- src/fn_native.rs | 4 +-- 8 files changed, 68 insertions(+), 29 deletions(-) create mode 100644 doc/src/patterns/dynamic-const.md diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index e7e02408..1e59b3e7 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -121,6 +121,7 @@ The Rhai Scripting Language 5. [Multi-Layer Functions](patterns/multi-layer.md) 6. [One Engine Instance Per Call](patterns/parallel.md) 7. [Scriptable Event Handler with State](patterns/events.md) + 8. [Dynamic Constants Provider](patterns/dynamic-const.md) 9. [Advanced Topics](advanced.md) 1. [Capture Scope for Function Call](language/fn-capture.md) 2. [Low-Level API](rust/register-raw.md) diff --git a/doc/src/advanced.md b/doc/src/advanced.md index 7ed51620..87136de0 100644 --- a/doc/src/advanced.md +++ b/doc/src/advanced.md @@ -3,14 +3,4 @@ Advanced Topics {{#include links.md}} -This section covers advanced features such as: - -* [Advanced patterns]({{rootUrl}}/patterns/index.md) in using Rhai. - -* [Capture the calling scope]({{rootUrl}}/language/fn-capture.md) in a function call. - -* [`serde`] integration. - -* Low-level [function registration API]({{rootUrl}}/rust/register-raw.md) - -* [Domain-Specific Languages][DSL]. +This section covers advanced features of the Rhai [`Engine`]. diff --git a/doc/src/engine/custom-syntax.md b/doc/src/engine/custom-syntax.md index 645c0684..f2a76db1 100644 --- a/doc/src/engine/custom-syntax.md +++ b/doc/src/engine/custom-syntax.md @@ -122,7 +122,7 @@ where: * `context: &mut EvalContext` - mutable reference to the current evaluation _context_ (**do not touch**) which exposes the following fields: * `context.engine(): &Engine` - reference to the current [`Engine`]. - * `context.namespace(): &Module` - reference to the current _global namespace_ 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. * `inputs: &[Expression]` - a list of input expression trees. diff --git a/doc/src/engine/var.md b/doc/src/engine/var.md index 6e9df16f..bcd75069 100644 --- a/doc/src/engine/var.md +++ b/doc/src/engine/var.md @@ -16,7 +16,7 @@ To do so, provide a closure to the [`Engine`] via the [`Engine::on_var`] method: let mut engine = Engine::new(); // Register a variable resolver. -engine.on_var(|name, index, engine, scope, lib| { +engine.on_var(|name, index, scope, context| { match name { "MYSTIC_NUMBER" => Ok(Some((42 as INT).into())), // Override a variable - make it not found even if it exists! @@ -50,26 +50,23 @@ Function Signature The function signature passed to `Engine::on_var` takes the following form: -> `Fn(name: &str, index: Option, scope: &Scope, context: &EvalContext) -> Result, Box> + 'static` +> `Fn(name: &str, index: usize, scope: &Scope, context: &EvalContext) -> Result, Box> + 'static` where: * `name: &str` - variable name. -* `index: Option` - an offset from the bottom of the current [`Scope`] that the variable is supposed to reside. +* `index: usize` - an offset from the bottom of the current [`Scope`] that the variable is supposed to reside. Offsets start from 1, with 1 meaning the last variable in the current [`Scope`]. Essentially the correct variable is at position `scope.len() - index`. - Notice that, if there was an [`eval`] statement before the current statement, new variables may have been introduced and this index may be incorrect. - Therefore, this index is for reference only. It should not be relied upon. - If `index` is `None`, then there is no pre-calculated offset position and a search through the current [`Scope`] must be performed. + 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. * `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_ 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. The return value is `Result, Box>` where `Ok(None)` indicates that the normal variable resolution process should continue. - diff --git a/doc/src/patterns/dynamic-const.md b/doc/src/patterns/dynamic-const.md new file mode 100644 index 00000000..b370e099 --- /dev/null +++ b/doc/src/patterns/dynamic-const.md @@ -0,0 +1,56 @@ +Dynamic Constants Provider +========================= + +{{#include ../links.md}} + + +Usage Scenario +-------------- + +* A system has a _large_ number of constants, but only a minor set will be used by any script. + +* The system constants are expensive to load. + +* The system constants set is too massive to push into a custom [`Scope`]. + +* The values of system constants are volatile and call-dependent. + + +Key Concepts +------------ + +* Use a [variable resolver] to intercept variable access. + +* Only load a variable when it is being used. + +* Perform a lookup based on variable name, and provide the correct data value. + +* May even perform back-end network access or look up the latest value from a database. + + +Implementation +-------------- + +```rust +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(); + +// Clone the shared provider +let db = provider.clone(); + +// Register a variable resolver. +// Move the shared provider into the closure. +engine.on_var(move |name, _, _, _| Ok(db.get(name).map(Dynamic::from))); +``` + + +Values are Constants +-------------------- + +All values provided by a [variable resolver] are _constants_ due to their dynamic nature. +They cannot be assigned to. + +In order to change values in an external system, register a dedicated API for that purpose. diff --git a/src/api.rs b/src/api.rs index 7b7e6d3d..f07b05c0 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1721,12 +1721,7 @@ impl Engine { #[inline(always)] pub fn on_var( &mut self, - callback: impl Fn( - &str, - Option, - &Scope, - &EvalContext, - ) -> Result, Box> + callback: impl Fn(&str, usize, &Scope, &EvalContext) -> Result, Box> + SendSync + 'static, ) -> &mut Self { diff --git a/src/engine.rs b/src/engine.rs index 9b5f4a53..5c47a3cf 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -745,7 +745,7 @@ impl Engine { this_ptr, level: 0, }; - if let Some(result) = resolve_var(name, index.map(|v| v.get()), scope, &context) + if let Some(result) = resolve_var(name, index.map_or(0, |v| v.get()), scope, &context) .map_err(|err| err.fill_position(*pos))? { return Ok((result.into(), name, ScopeEntryType::Constant, *pos)); diff --git a/src/fn_native.rs b/src/fn_native.rs index 8cef4998..3aa5e7a4 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -224,13 +224,13 @@ pub type Callback = Box R + Send + Sync + 'static>; /// A standard callback function. #[cfg(not(feature = "sync"))] pub type OnVarCallback = Box< - dyn Fn(&str, Option, &Scope, &EvalContext) -> Result, Box> + dyn Fn(&str, usize, &Scope, &EvalContext) -> Result, Box> + 'static, >; /// A standard callback function. #[cfg(feature = "sync")] pub type OnVarCallback = Box< - dyn Fn(&str, Option, &Scope, &EvalContext) -> Result, Box> + dyn Fn(&str, usize, &Scope, &EvalContext) -> Result, Box> + Send + Sync + 'static,