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

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