Implement variable resolver.

This commit is contained in:
Stephen Chung 2020-10-11 21:58:11 +08:00
parent 9d93dac8e7
commit fd5a932611
18 changed files with 511 additions and 237 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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<Dynamic, Box<EvalAltResult>>`
> `Fn(scope: &mut Scope, context: &mut EvalContext, inputs: &[Expression]) -> Result<Dynamic, Box<EvalAltResult>>`
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<Dynamic, Box<EvalAltResult>> {
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;

75
doc/src/engine/var.md Normal file
View File

@ -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<usize>, scope: &Scope, context: &EvalContext) -> Result<Option<Dynamic>, Box<EvalAltResult>> + 'static`
where:
* `name: &str` - variable name.
* `index: Option<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.
* `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<Option<Dynamic>, Box<EvalAltResult>>` where `Ok(None)` indicates that the normal
variable resolution process should continue.

View File

@ -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

View File

@ -3,6 +3,7 @@ Maximum Number of Operations
{{#include ../links.md}}
Limit How Long a Script Can Run
------------------------------

View File

@ -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();

View File

@ -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<rhai::EvalAltResult>> {
/// 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::<i64>("MYSTIC_NUMBER")?;
///
/// # Ok(())
/// # }
/// ```
#[inline(always)]
pub fn on_var(
&mut self,
callback: impl Fn(
&str,
Option<usize>,
&Scope,
&EvalContext,
) -> Result<Option<Dynamic>, Box<EvalAltResult>>
+ SendSync
+ 'static,
) -> &mut Self {
self.resolve_var = Some(Box::new(callback));
self
}
/// Register a callback for script evaluation progress.
///
/// # Example

View File

@ -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<HashMap<String, u8>>,
/// Custom syntax.
pub(crate) custom_syntax: Option<HashMap<String, CustomSyntax>>,
/// Callback closure for resolving variable access.
pub(crate) resolve_var: Option<OnVarCallback>,
/// Callback closure for implementing the `print` command.
pub(crate) print: Callback<str, ()>,
@ -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<EvalAltResult>> {
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<EvalAltResult>> {
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<EvalAltResult>> {
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<EvalAltResult>> {
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::<Map>() => {
@ -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<EvalAltResult>> {
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<Dynamic, Box<EvalAltResult>> {
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<Dynamic, Box<EvalAltResult>> {
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::<Dynamic>().unwrap() = value;
*lhs_ptr.as_mut().write_lock::<Dynamic>().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::<Dynamic>().unwrap();
lock_guard = lhs_ptr.as_mut().write_lock::<Dynamic>().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::<Dynamic>().unwrap() = value;
*lhs_ptr.as_mut().write_lock::<Dynamic>().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::<StaticVec<_>>();
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<Dynamic, Box<EvalAltResult>> {
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.

View File

@ -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::<Dynamic>::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::<Result<_, _>>()?;
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::<Result<_, _>>()?;
// 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, ...)
_ => {

View File

@ -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<T, R> = Box<dyn Fn(&T) -> R + 'static>;
#[cfg(feature = "sync")]
pub type Callback<T, R> = Box<dyn Fn(&T) -> R + Send + Sync + 'static>;
/// A standard callback function.
#[cfg(not(feature = "sync"))]
pub type OnVarCallback = Box<
dyn Fn(&str, Option<usize>, &Scope, &EvalContext) -> Result<Option<Dynamic>, Box<EvalAltResult>>
+ 'static,
>;
/// A standard callback function.
#[cfg(feature = "sync")]
pub type OnVarCallback = Box<
dyn Fn(&str, Option<usize>, &Scope, &EvalContext) -> Result<Option<Dynamic>, Box<EvalAltResult>>
+ Send
+ Sync
+ 'static,
>;
/// A type encapsulating a function callable by Rhai.
#[derive(Clone)]
pub enum CallableFunction {

View File

@ -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")]

View File

@ -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()
})
},
);

View File

@ -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<Self>, new_position: Position) -> Box<Self> {
pub(crate) fn fill_position(mut self: Box<Self>, new_position: Position) -> Box<Self> {
if self.position().is_none() {
self.set_position(new_position);
}

View File

@ -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<Dynamic, Box<EvalAltResult>>;
pub type FnCustomSyntaxEval =
dyn Fn(&mut Scope, &mut EvalContext, &[Expression]) -> Result<Dynamic, Box<EvalAltResult>>;
/// A general expression evaluation trait object.
#[cfg(feature = "sync")]
pub type FnCustomSyntaxEval = dyn Fn(&Engine, &mut EvalContext, &mut Scope, &[Expression]) -> Result<Dynamic, Box<EvalAltResult>>
pub type FnCustomSyntaxEval = dyn Fn(&mut Scope, &mut EvalContext, &[Expression]) -> Result<Dynamic, Box<EvalAltResult>>
+ 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<Dynamic, Box<EvalAltResult>> {
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<String>,
@ -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<S: AsRef<str> + ToString>(
&mut self,
keywords: &[S],
scope_delta: isize,
func: impl Fn(
&Engine,
&mut EvalContext,
&mut Scope,
&[Expression],
) -> Result<Dynamic, Box<EvalAltResult>>
new_vars: isize,
func: impl Fn(&mut Scope, &mut EvalContext, &[Expression]) -> Result<Dynamic, Box<EvalAltResult>>
+ SendSync
+ 'static,
) -> Result<&mut Self, ParseError> {
@ -176,7 +185,7 @@ impl Engine {
let syntax = CustomSyntax {
segments,
func: (Box::new(func) as Box<FnCustomSyntaxEval>).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<Dynamic, Box<EvalAltResult>> {
self.eval_expr(
scope,
context.mods,
context.state,
context.lib,
context.this_ptr,
expr.expr(),
context.level,
)
}
}

View File

@ -22,21 +22,28 @@ fn test_custom_syntax() -> Result<(), Box<EvalAltResult>> {
"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::<bool>(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<EvalAltResult>> {
// 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())

View File

@ -9,7 +9,7 @@ fn test_unary_minus() -> Result<(), Box<EvalAltResult>> {
#[cfg(not(feature = "no_function"))]
assert_eq!(engine.eval::<INT>("fn neg(x) { -x } neg(5)")?, -5);
assert_eq!(engine.eval::<INT>("5 - -+++--+-5")?, 0);
assert_eq!(engine.eval::<INT>("5 - -+ + + - -+-5")?, 0);
Ok(())
}

View File

@ -1,4 +1,4 @@
use rhai::{Engine, EvalAltResult, Scope, INT};
use rhai::{Engine, EvalAltResult, Position, Scope, INT};
#[test]
fn test_var_scope() -> Result<(), Box<EvalAltResult>> {
@ -52,3 +52,41 @@ fn test_scope_eval() -> Result<(), Box<EvalAltResult>> {
Ok(())
}
#[test]
fn test_var_resolver() -> Result<(), Box<EvalAltResult>> {
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::<INT>(&mut scope, "MYSTIC_NUMBER")?,
42
);
assert_eq!(engine.eval_with_scope::<INT>(&mut scope, "chameleon")?, 1);
assert!(
matches!(*engine.eval_with_scope::<INT>(&mut scope, "DO_NOT_USE").expect_err("should error"),
EvalAltResult::ErrorVariableNotFound(n, _) if n == "DO_NOT_USE")
);
Ok(())
}