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