Merge pull request #110 from schungx/master

eval
This commit is contained in:
Stephen Chung 2020-03-21 00:27:38 +08:00 committed by GitHub
commit 9c7fa97171
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 715 additions and 329 deletions

134
README.md
View File

@ -273,10 +273,11 @@ type_of('c') == "char";
type_of(42) == "i64";
let x = 123;
x.type_of() == "i64";
x.type_of(); // error - 'type_of' cannot use postfix notation
type_of(x) == "i64";
x = 99.999;
x.type_of() == "f64";
type_of(x) == "f64";
x = "hello";
if type_of(x) == "string" {
@ -538,12 +539,12 @@ with a special "pretty-print" name, [`type_of`] will return that name instead.
engine.register_type::<TestStruct>();
engine.register_fn("new_ts", TestStruct::new);
let x = new_ts();
print(x.type_of()); // prints "path::to::module::TestStruct"
print(type_of(x)); // prints "path::to::module::TestStruct"
engine.register_type_with_name::<TestStruct>("Hello");
engine.register_fn("new_ts", TestStruct::new);
let x = new_ts();
print(x.type_of()); // prints "Hello"
print(type_of(x)); // prints "Hello"
```
Getters and setters
@ -1304,7 +1305,8 @@ for entry in log {
Script optimization
===================
Rhai includes an _optimizer_ that tries to optimize a script after parsing. This can reduce resource utilization and increase execution speed.
Rhai includes an _optimizer_ that tries to optimize a script after parsing.
This can reduce resource utilization and increase execution speed.
Script optimization can be turned off via the [`no_optimize`] feature.
For example, in the following:
@ -1315,7 +1317,7 @@ For example, in the following:
123; // eliminated - no effect
"hello"; // eliminated - no effect
[1, 2, x, x*2, 5]; // eliminated - no effect
foo(42); // NOT eliminated - the function 'foo' may have side effects
foo(42); // NOT eliminated - functions calls are not touched
666 // NOT eliminated - this is the return value of the block,
// and the block is the last one
// so this is the return value of the whole script
@ -1349,14 +1351,12 @@ are spliced into the script text in order to turn on/off certain sections.
For fixed script texts, the constant values can be provided in a user-defined [`Scope`] object
to the [`Engine`] for use in compilation and evaluation.
Beware, however, that most operators are actually function calls, and those functions can be overridden,
so they are not optimized away:
Beware, however, that most operators are actually function calls, and those are not optimized away:
```rust
const DECISION = 1;
if DECISION == 1 { // NOT optimized away because you can define
: // your own '==' function to override the built-in default!
if DECISION == 1 { // NOT optimized away because it requires a call to the '==' function
:
} else if DECISION == 2 { // same here, NOT optimized away
:
@ -1367,8 +1367,8 @@ if DECISION == 1 { // NOT optimized away because you can define
}
```
because no operator functions will be run (in order not to trigger side effects) during the optimization process
(unless the optimization level is set to [`OptimizationLevel::Full`]). So, instead, do this:
because no operator functions will be run during the optimization process (unless the optimization level is
set to [`OptimizationLevel::Full`]). So, instead, do this:
```rust
const DECISION_1 = true;
@ -1405,11 +1405,14 @@ There are actually three levels of optimizations: `None`, `Simple` and `Full`.
* `None` is obvious - no optimization on the AST is performed.
* `Simple` (default) performs relatively _safe_ optimizations without causing side effects
(i.e. it only relies on static analysis and will not actually perform any function calls).
* `Simple` (default) relies exclusively on static analysis, performing relatively _safe_ optimizations only.
In particular, no function calls will be made to determine the output value. This also means that most comparison
operators and constant arithmetic expressions are untouched.
* `Full` is _much_ more aggressive, _including_ running functions on constant arguments to determine their result.
One benefit to this is that many more optimization opportunities arise, especially with regards to comparison operators.
* `Full` is _much_ more aggressive. Functions _will_ be run, when passed constant arguments, to determine their results.
One benefit is that many more optimization opportunities arise, especially with regards to comparison operators.
Nevertheless, the majority of scripts do not excessively rely on constants, and that is why this optimization level
is opt-in; only scripts that are machine-generated tend to have constants spliced in at generation time.
An [`Engine`]'s optimization level is set via a call to `set_optimization_level`:
@ -1419,15 +1422,15 @@ engine.set_optimization_level(rhai::OptimizationLevel::Full);
```
When the optimization level is [`OptimizationLevel::Full`], the [`Engine`] assumes all functions to be _pure_ and will _eagerly_
evaluated all function calls with constant arguments, using the result to replace the call. This also applies to all operators
(which are implemented as functions). For instance, the same example above:
call all functions when passed constant arguments, using the results to replace the actual calls. This also affects all operators
because most of them are implemented as functions. For instance, the same example above:
```rust
// When compiling the following with OptimizationLevel::Full...
const DECISION = 1;
// this condition is now eliminated because 'DECISION == 1'
if DECISION == 1 { // is a function call to the '==' function, and it returns 'true'
// this condition is now eliminated because 'DECISION == 1' is a
if DECISION == 1 { // function call to the '==' function with constant arguments, and it returns 'true'
print("hello!"); // this block is promoted to the parent level
} else {
print("boo!"); // this block is eliminated because it is never reached
@ -1446,25 +1449,20 @@ let x = (1 + 2) * 3 - 4 / 5 % 6; // <- will be replaced by 'let x = 9'
let y = (1 > 2) || (3 <= 4); // <- will be replaced by 'let y = true'
```
Function side effect considerations
----------------------------------
All of Rhai's built-in functions (and operators which are implemented as functions) are _pure_ (i.e. they do not mutate state
nor cause side any effects, with the exception of `print` and `debug` which are handled specially) so using [`OptimizationLevel::Full`]
is usually quite safe _unless_ you register your own types and functions.
If custom functions are registered, they _may_ be called (or maybe not, if the calls happen to lie within a pruned code block).
If custom functions are registered to replace built-in operators, they will also be called when the operators are used (in an `if`
statement, for example) and cause side-effects.
Function volatility considerations
---------------------------------
Even if a custom function does not mutate state nor cause side effects, it may still be _volatile_, i.e. it _depends_ on the external
environment and not _pure_. A perfect example is a function that gets the current time - obviously each run will return a different value!
The optimizer, when using [`OptimizationLevel::Full`], _assumes_ that all functions are _pure_, so when it finds constant arguments.
This may cause the script to behave differently from the intended semantics because essentially the result of each function call will
always be the same value.
Rhai functions never mutate state nor cause side any effects (except `print` and `debug` which are handled specially).
The only functions allowed to mutate state are custom type getters, setters and methods, and functions calls involving custom types
are never optimized. So using [`OptimizationLevel::Full`] is usually quite safe.
However, even if a function cannot mutate state nor cause side effects, it may still be _volatile_, i.e. it may
_depend_ on the external environment and not be _pure_. A perfect example is a function that gets the current time -
obviously each run will return a different value!
The optimizer, when using [`OptimizationLevel::Full`], _assumes_ that all functions are _pure_ when it finds constant arguments.
This may cause the script to behave differently from the intended semantics because essentially the result of each function call
will always be the same value.
Therefore, **avoid using [`OptimizationLevel::Full`]** if you intend to register non-_pure_ custom types and/or functions.
@ -1515,3 +1513,67 @@ let engine = rhai::Engine::new();
// Turn off the optimizer
engine.set_optimization_level(rhai::OptimizationLevel::None);
```
`eval` - or "How to Shoot Yourself in the Foot even Easier"
---------------------------------------------------------
Saving the best for last: in addition to script optimizations, there is the ever-dreaded... `eval` function!
```rust
let x = 10;
fn foo(x) { x += 12; x }
let script = "let y = x;"; // build a script
script += "y += foo(y);";
script += "x + y";
let result = eval(script); // <- look, JS, we can also do this!
print("Answer: " + result); // prints 42
print("x = " + x); // prints 10 - functions call arguments are passed by value
print("y = " + y); // prints 32 - variables defined in 'eval' persist!
eval("{ let z = y }"); // to keep a variable local, use a statement block
print("z = " + z); // error - variable 'z' not found
"print(42)".eval(); // nope - just like 'type_of' postfix notation doesn't work
```
Script segments passed to `eval` execute inside the current [`Scope`], so they can access and modify _everything_,
including all variables that are visible at that position in code! It is almost as if the script segments were
physically pasted in at the position of the `eval` call.
```rust
let script = "x += 32";
let x = 10;
eval(script); // variable 'x' in the current scope is visible!
print(x); // prints 42
// The above is equivalent to:
let script = "x += 32";
let x = 10;
x += 32;
print(x);
```
For those who subscribe to the (very sensible) motto of ["`eval` is **evil**"](http://linterrors.com/js/eval-is-evil),
disable `eval` by overriding it, probably with something that throws.
```rust
fn eval(script) { throw "eval is evil! I refuse to run " + script }
let x = eval("40 + 2"); // 'eval' here throws "eval is evil! I refuse to run 40 + 2"
```
Or override it from Rust:
```rust
fn alt_eval(script: String) -> Result<(), EvalAltResult> {
Err(format!("eval is evil! I refuse to run {}", script).into())
}
engine.register_result_fn("eval", alt_eval);
```

View File

@ -2,7 +2,7 @@
use crate::any::{Any, AnyExt, Dynamic};
use crate::call::FuncArgs;
use crate::engine::{Engine, FnAny, FnSpec};
use crate::engine::{Engine, FnAny, FnSpec, FUNC_GETTER, FUNC_SETTER};
use crate::error::ParseError;
use crate::fn_register::RegisterFn;
use crate::parser::{lex, parse, FnDef, Position, AST};
@ -173,8 +173,8 @@ impl<'e> Engine<'e> {
name: &str,
callback: impl Fn(&mut T) -> U + 'static,
) {
let get_name = "get$".to_string() + name;
self.register_fn(&get_name, callback);
let get_fn_name = format!("{}{}", FUNC_GETTER, name);
self.register_fn(&get_fn_name, callback);
}
/// Register a setter function for a member of a registered type with the `Engine`.
@ -215,8 +215,8 @@ impl<'e> Engine<'e> {
name: &str,
callback: impl Fn(&mut T, U) -> () + 'static,
) {
let set_name = "set$".to_string() + name;
self.register_fn(&set_name, callback);
let set_fn_name = format!("{}{}", FUNC_SETTER, name);
self.register_fn(&set_fn_name, callback);
}
/// Shorthand for registering both getter and setter functions
@ -264,7 +264,7 @@ impl<'e> Engine<'e> {
self.register_set(name, set_fn);
}
/// Compile a string into an `AST`, which can be used later for evaluations.
/// Compile a string into an `AST`, which can be used later for evaluation.
///
/// # Example
///
@ -274,7 +274,7 @@ impl<'e> Engine<'e> {
///
/// let mut engine = Engine::new();
///
/// // Compile a script to an AST and store it for later evaluations
/// // Compile a script to an AST and store it for later evaluation
/// let ast = engine.compile("40 + 2")?;
///
/// for _ in 0..42 {
@ -287,27 +287,40 @@ impl<'e> Engine<'e> {
self.compile_with_scope(&Scope::new(), input)
}
/// Compile a string into an `AST` using own scope, which can be used later for evaluations.
/// The scope is useful for passing constants into the script for optimization.
/// Compile a string into an `AST` using own scope, which can be used later for evaluation.
/// The scope is useful for passing constants into the script for optimization
/// when using `OptimizationLevel::Full`.
///
/// # Example
///
/// ```
/// # fn main() -> Result<(), rhai::EvalAltResult> {
/// use rhai::{Engine, Scope};
/// # #[cfg(not(feature = "no_optimize"))]
/// # {
/// use rhai::{Engine, Scope, OptimizationLevel};
///
/// let mut engine = Engine::new();
///
/// // Set optimization level to 'Full' so the Engine can fold constants
/// // into function calls and operators.
/// engine.set_optimization_level(OptimizationLevel::Full);
///
/// // Create initialized scope
/// let mut scope = Scope::new();
/// scope.push_constant("x", 42_i64); // 'x' is a constant
///
/// // Compile a script to an AST and store it for later evaluations
/// // Compile a script to an AST and store it for later evaluation.
/// // Notice that `Full` optimization is on, so constants are folded
/// // into function calls and operators.
/// let ast = engine.compile_with_scope(&mut scope,
/// "if x > 40 { x } else { 0 }"
/// "if x > 40 { x } else { 0 }" // all 'x' are replaced with 42
/// )?;
///
/// // Normally this would have failed because no scope is passed into the 'eval_ast'
/// // call and so the variable 'x' does not exist. Here, it passes because the script
/// // has been optimized and all references to 'x' are already gone.
/// assert_eq!(engine.eval_ast::<i64>(&ast)?, 42);
/// # }
/// # Ok(())
/// # }
/// ```
@ -329,7 +342,7 @@ impl<'e> Engine<'e> {
.map(|_| contents)
}
/// Compile a script file into an `AST`, which can be used later for evaluations.
/// Compile a script file into an `AST`, which can be used later for evaluation.
///
/// # Example
///
@ -339,7 +352,7 @@ impl<'e> Engine<'e> {
///
/// let mut engine = Engine::new();
///
/// // Compile a script file to an AST and store it for later evaluations
/// // Compile a script file to an AST and store it for later evaluation.
/// // Notice that a PathBuf is required which can easily be constructed from a string.
/// let ast = engine.compile_file("script.rhai".into())?;
///
@ -354,26 +367,33 @@ impl<'e> Engine<'e> {
self.compile_file_with_scope(&Scope::new(), path)
}
/// Compile a script file into an `AST` using own scope, which can be used later for evaluations.
/// The scope is useful for passing constants into the script for optimization.
/// Compile a script file into an `AST` using own scope, which can be used later for evaluation.
/// The scope is useful for passing constants into the script for optimization
/// when using `OptimizationLevel::Full`.
///
/// # Example
///
/// ```no_run
/// # fn main() -> Result<(), rhai::EvalAltResult> {
/// use rhai::{Engine, Scope};
/// # #[cfg(not(feature = "no_optimize"))]
/// # {
/// use rhai::{Engine, Scope, OptimizationLevel};
///
/// let mut engine = Engine::new();
///
/// // Set optimization level to 'Full' so the Engine can fold constants.
/// engine.set_optimization_level(OptimizationLevel::Full);
///
/// // Create initialized scope
/// let mut scope = Scope::new();
/// scope.push_constant("x", 42_i64); // 'x' is a constant
///
/// // Compile a script to an AST and store it for later evaluations
/// // Compile a script to an AST and store it for later evaluation.
/// // Notice that a PathBuf is required which can easily be constructed from a string.
/// let ast = engine.compile_file_with_scope(&mut scope, "script.rhai".into())?;
///
/// let result = engine.eval_ast::<i64>(&ast)?;
/// # }
/// # Ok(())
/// # }
/// ```
@ -491,13 +511,13 @@ impl<'e> Engine<'e> {
///
/// # Example
///
/// ```no_run
/// ```
/// # fn main() -> Result<(), rhai::EvalAltResult> {
/// use rhai::Engine;
///
/// let mut engine = Engine::new();
///
/// // Compile a script to an AST and store it for later evaluations
/// // Compile a script to an AST and store it for later evaluation
/// let ast = engine.compile("40 + 2")?;
///
/// // Evaluate it
@ -514,22 +534,25 @@ impl<'e> Engine<'e> {
///
/// # Example
///
/// ```no_run
/// ```
/// # fn main() -> Result<(), rhai::EvalAltResult> {
/// use rhai::{Engine, Scope};
///
/// let mut engine = Engine::new();
///
/// // Compile a script to an AST and store it for later evaluations
/// // Compile a script to an AST and store it for later evaluation
/// let ast = engine.compile("x + 2")?;
///
/// // Create initialized scope
/// let mut scope = Scope::new();
/// scope.push("x", 40_i64);
///
/// // Compile a script to an AST and store it for later evaluation
/// let ast = engine.compile("x = x + 2; x")?;
///
/// // Evaluate it
/// assert_eq!(engine.eval_with_scope::<i64>(&mut scope, "x = x + 2; x")?, 42);
/// assert_eq!(engine.eval_with_scope::<i64>(&mut scope, "x = x + 2; x")?, 44);
/// assert_eq!(engine.eval_ast_with_scope::<i64>(&mut scope, &ast)?, 42);
/// assert_eq!(engine.eval_ast_with_scope::<i64>(&mut scope, &ast)?, 44);
///
/// // The variable in the scope is modified
/// assert_eq!(scope.get_value::<i64>("x").expect("variable x should exist"), 44);
@ -541,12 +564,32 @@ impl<'e> Engine<'e> {
scope: &mut Scope,
ast: &AST,
) -> Result<T, EvalAltResult> {
self.eval_ast_with_scope_raw(scope, false, ast)
.and_then(|out| {
out.downcast::<T>().map(|v| *v).map_err(|a| {
EvalAltResult::ErrorMismatchOutputType(
self.map_type_name((*a).type_name()).to_string(),
Position::none(),
)
})
})
}
pub(crate) fn eval_ast_with_scope_raw(
&mut self,
scope: &mut Scope,
retain_functions: bool,
ast: &AST,
) -> Result<Dynamic, EvalAltResult> {
fn eval_ast_internal(
engine: &mut Engine,
scope: &mut Scope,
retain_functions: bool,
ast: &AST,
) -> Result<Dynamic, EvalAltResult> {
if !retain_functions {
engine.clear_functions();
}
let statements = {
let AST(statements, functions) = ast;
@ -560,24 +603,17 @@ impl<'e> Engine<'e> {
result = engine.eval_stmt(scope, stmt)?;
}
if !retain_functions {
engine.clear_functions();
}
Ok(result)
}
eval_ast_internal(self, scope, ast)
.or_else(|err| match err {
eval_ast_internal(self, scope, retain_functions, ast).or_else(|err| match err {
EvalAltResult::Return(out, _) => Ok(out),
_ => Err(err),
})
.and_then(|out| {
out.downcast::<T>().map(|v| *v).map_err(|a| {
EvalAltResult::ErrorMismatchOutputType(
self.map_type_name((*a).type_name()).to_string(),
Position::eof(),
)
})
})
}
/// Evaluate a file, but throw away the result and only return error (if any).

View File

@ -3,7 +3,7 @@
use crate::any::{Any, AnyExt, Dynamic, Variant};
use crate::parser::{Expr, FnDef, Position, ReturnType, Stmt};
use crate::result::EvalAltResult;
use crate::scope::{Scope, VariableType};
use crate::scope::{Scope, ScopeSource, VariableType};
#[cfg(not(feature = "no_optimize"))]
use crate::optimize::OptimizationLevel;
@ -38,6 +38,7 @@ pub(crate) const KEYWORD_PRINT: &'static str = "print";
pub(crate) const KEYWORD_DEBUG: &'static str = "debug";
pub(crate) const KEYWORD_DUMP_AST: &'static str = "dump_ast";
pub(crate) const KEYWORD_TYPE_OF: &'static str = "type_of";
pub(crate) const KEYWORD_EVAL: &'static str = "eval";
pub(crate) const FUNC_GETTER: &'static str = "get$";
pub(crate) const FUNC_SETTER: &'static str = "set$";
@ -185,10 +186,15 @@ impl Engine<'_> {
// Evaluate
// Convert return statement to return value
return match self.eval_stmt(&mut scope, &fn_def.body) {
Err(EvalAltResult::Return(x, _)) => Ok(x),
other => other,
};
return self
.eval_stmt(&mut scope, &fn_def.body)
.or_else(|mut err| match err {
EvalAltResult::Return(x, _) => Ok(x),
_ => {
err.set_position(pos);
Err(err)
}
});
}
let spec = FnSpec {
@ -196,32 +202,26 @@ impl Engine<'_> {
args: Some(args.iter().map(|a| Any::type_id(&**a)).collect()),
};
// Argument must be a string
fn cast_to_string<'a>(r: &'a Variant, pos: Position) -> Result<&'a str, EvalAltResult> {
r.downcast_ref::<String>()
.map(String::as_str)
.ok_or_else(|| EvalAltResult::ErrorMismatchOutputType(r.type_name().into(), pos))
}
// Search built-in's and external functions
if let Some(func) = self.ext_functions.get(&spec) {
// Run external function
let result = func(args, pos)?;
// See if the function match print/debug (which requires special processing)
let callback = match fn_name {
KEYWORD_PRINT => self.on_print.as_mut(),
KEYWORD_DEBUG => self.on_debug.as_mut(),
_ => return Ok(result),
};
let val = &result
.downcast::<String>()
.map(|s| *s)
.unwrap_or("error: not a string".into());
return Ok(callback(val).into_dynamic());
match fn_name {
KEYWORD_PRINT => self.on_print.as_mut()(cast_to_string(result.as_ref(), pos)?),
KEYWORD_DEBUG => self.on_debug.as_mut()(cast_to_string(result.as_ref(), pos)?),
_ => (),
}
if fn_name == KEYWORD_TYPE_OF && args.len() == 1 {
// Handle `type_of` function
return Ok(self
.map_type_name(args[0].type_name())
.to_string()
.into_dynamic());
return Ok(result);
}
if fn_name.starts_with(FUNC_GETTER) {
@ -264,13 +264,38 @@ impl Engine<'_> {
))
}
/// Chain-evaluate a dot setter
/// Chain-evaluate a dot setter.
///
/// Either `src` or `target` should be `Some`.
///
/// If `target` is `Some`, then it is taken as the reference to use for `this`.
///
/// Otherwise, if `src` is `Some`, then it holds a name and index into `scope`; using `get_mut` on
/// `scope` can retrieve a mutable reference to the variable's value to use as `this`.
fn get_dot_val_helper(
&mut self,
scope: &mut Scope,
this_ptr: &mut Variant,
src: Option<ScopeSource>,
target: Option<&mut Variant>,
dot_rhs: &Expr,
) -> Result<Dynamic, EvalAltResult> {
// Get the `this` reference. Either `src` or `target` should be `Some`.
fn get_this_ptr<'a>(
scope: &'a mut Scope,
src: Option<ScopeSource>,
target: Option<&'a mut Variant>,
) -> &'a mut Variant {
if let Some(t) = target {
// If `target` is `Some`, then it is returned.
t
} else {
// Otherwise, if `src` is `Some`, then it holds a name and index into `scope`;
// using `get_mut` on `scope` to retrieve a mutable reference for return.
let ScopeSource { name, idx, .. } = src.expect("expected source in scope");
scope.get_mut(name, idx).as_mut()
}
}
match dot_rhs {
// xxx.fn_name(args)
Expr::FunctionCall(fn_name, arg_expr_list, def_val, pos) => {
@ -279,6 +304,8 @@ impl Engine<'_> {
.map(|arg_expr| self.eval_expr(scope, arg_expr))
.collect::<Result<Vec<_>, _>>()?;
let this_ptr = get_this_ptr(scope, src, target);
let args = once(this_ptr)
.chain(values.iter_mut().map(Dynamic::as_mut))
.collect();
@ -289,26 +316,28 @@ impl Engine<'_> {
// xxx.id
Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
let this_ptr = get_this_ptr(scope, src, target);
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
}
// xxx.idx_lhs[idx_expr]
#[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, idx_expr, idx_pos) => {
Expr::Index(idx_lhs, idx_expr, op_pos) => {
let (val, _) = match idx_lhs.as_ref() {
// xxx.id[idx_expr]
Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
let this_ptr = get_this_ptr(scope, src, target);
(
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)?,
*pos,
)
}
// xxx.???[???][idx_expr]
Expr::Index(_, _, _) => {
(self.get_dot_val_helper(scope, this_ptr, idx_lhs)?, *idx_pos)
}
Expr::Index(_, _, _) => (
self.get_dot_val_helper(scope, src, target, idx_lhs)?,
*op_pos,
),
// Syntax error
_ => {
return Err(EvalAltResult::ErrorDotExpr(
@ -319,7 +348,7 @@ impl Engine<'_> {
};
let idx = self.eval_index_value(scope, idx_expr)?;
self.get_indexed_value(&val, idx, idx_expr.position(), *idx_pos)
self.get_indexed_value(&val, idx, idx_expr.position(), *op_pos)
.map(|(v, _)| v)
}
@ -328,26 +357,31 @@ impl Engine<'_> {
// xxx.id.rhs
Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
let this_ptr = get_this_ptr(scope, src, target);
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
.and_then(|mut v| self.get_dot_val_helper(scope, v.as_mut(), rhs))
.and_then(|mut v| {
self.get_dot_val_helper(scope, None, Some(v.as_mut()), rhs)
})
}
// xxx.idx_lhs[idx_expr].rhs
#[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, idx_expr, idx_pos) => {
Expr::Index(idx_lhs, idx_expr, op_pos) => {
let (val, _) = match idx_lhs.as_ref() {
// xxx.id[idx_expr].rhs
Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
let this_ptr = get_this_ptr(scope, src, target);
(
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)?,
*pos,
)
}
// xxx.???[???][idx_expr].rhs
Expr::Index(_, _, _) => {
(self.get_dot_val_helper(scope, this_ptr, idx_lhs)?, *idx_pos)
}
Expr::Index(_, _, _) => (
self.get_dot_val_helper(scope, src, target, idx_lhs)?,
*op_pos,
),
// Syntax error
_ => {
return Err(EvalAltResult::ErrorDotExpr(
@ -358,8 +392,10 @@ impl Engine<'_> {
};
let idx = self.eval_index_value(scope, idx_expr)?;
self.get_indexed_value(&val, idx, idx_expr.position(), *idx_pos)
.and_then(|(mut v, _)| self.get_dot_val_helper(scope, v.as_mut(), rhs))
self.get_indexed_value(&val, idx, idx_expr.position(), *op_pos)
.and_then(|(mut v, _)| {
self.get_dot_val_helper(scope, None, Some(v.as_mut()), rhs)
})
}
// Syntax error
_ => Err(EvalAltResult::ErrorDotExpr(
@ -386,28 +422,33 @@ impl Engine<'_> {
match dot_lhs {
// id.???
Expr::Variable(id, pos) => {
let (src_idx, _, mut target) = Self::search_scope(scope, id, Ok, *pos)?;
let val = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs);
let (ScopeSource { idx, var_type, .. }, _) =
Self::search_scope(scope, id, Ok, *pos)?;
// In case the expression mutated `target`, we need to update it back into the scope because it is cloned.
*scope.get_mut(id, src_idx) = target;
// This is a variable property access (potential function call).
// Use a direct index into `scope` to directly mutate the variable value.
let src = ScopeSource {
name: id,
idx,
var_type,
};
val
self.get_dot_val_helper(scope, Some(src), None, dot_rhs)
}
// idx_lhs[idx_expr].???
#[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, idx_expr, idx_pos) => {
Expr::Index(idx_lhs, idx_expr, op_pos) => {
let (src_type, src, idx, mut target) =
self.eval_index_expr(scope, idx_lhs, idx_expr, *idx_pos)?;
let val = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs);
self.eval_index_expr(scope, idx_lhs, idx_expr, *op_pos)?;
let val = self.get_dot_val_helper(scope, None, Some(target.as_mut()), dot_rhs);
// In case the expression mutated `target`, we need to update it back into the scope because it is cloned.
if let Some((id, var_type, src_idx)) = src {
match var_type {
if let Some(src) = src {
match src.var_type {
VariableType::Constant => {
return Err(EvalAltResult::ErrorAssignmentToConstant(
id.to_string(),
src.name.to_string(),
idx_lhs.position(),
));
}
@ -415,11 +456,9 @@ impl Engine<'_> {
Self::update_indexed_var_in_scope(
src_type,
scope,
id,
src_idx,
src,
idx,
target,
dot_rhs.position(),
(target, dot_rhs.position()),
)?;
}
}
@ -431,22 +470,22 @@ impl Engine<'_> {
// {expr}.???
expr => {
let mut target = self.eval_expr(scope, expr)?;
self.get_dot_val_helper(scope, target.as_mut(), dot_rhs)
self.get_dot_val_helper(scope, None, Some(target.as_mut()), dot_rhs)
}
}
}
/// Search for a variable within the scope, returning its value and index inside the Scope
fn search_scope<T>(
scope: &Scope,
fn search_scope<'a, T>(
scope: &'a Scope,
id: &str,
map: impl FnOnce(Dynamic) -> Result<T, EvalAltResult>,
convert: impl FnOnce(Dynamic) -> Result<T, EvalAltResult>,
begin: Position,
) -> Result<(usize, VariableType, T), EvalAltResult> {
) -> Result<(ScopeSource<'a>, T), EvalAltResult> {
scope
.get(id)
.ok_or_else(|| EvalAltResult::ErrorVariableNotFound(id.into(), begin))
.and_then(move |(idx, _, var_type, val)| map(val).map(|v| (idx, var_type, v)))
.and_then(move |(src, value)| convert(value).map(|v| (src, v)))
}
/// Evaluate the value of an index (must evaluate to INT)
@ -468,8 +507,8 @@ impl Engine<'_> {
&self,
val: &Dynamic,
idx: INT,
val_pos: Position,
idx_pos: Position,
op_pos: Position,
) -> Result<(Dynamic, IndexSourceType), EvalAltResult> {
if val.is::<Array>() {
// val_array[idx]
@ -479,9 +518,9 @@ impl Engine<'_> {
arr.get(idx as usize)
.cloned()
.map(|v| (v, IndexSourceType::Array))
.ok_or_else(|| EvalAltResult::ErrorArrayBounds(arr.len(), idx, val_pos))
.ok_or_else(|| EvalAltResult::ErrorArrayBounds(arr.len(), idx, idx_pos))
} else {
Err(EvalAltResult::ErrorArrayBounds(arr.len(), idx, val_pos))
Err(EvalAltResult::ErrorArrayBounds(arr.len(), idx, idx_pos))
}
} else if val.is::<String>() {
// val_string[idx]
@ -492,20 +531,20 @@ impl Engine<'_> {
.nth(idx as usize)
.map(|ch| (ch.into_dynamic(), IndexSourceType::String))
.ok_or_else(|| {
EvalAltResult::ErrorStringBounds(s.chars().count(), idx, val_pos)
EvalAltResult::ErrorStringBounds(s.chars().count(), idx, idx_pos)
})
} else {
Err(EvalAltResult::ErrorStringBounds(
s.chars().count(),
idx,
val_pos,
idx_pos,
))
}
} else {
// Error - cannot be indexed
Err(EvalAltResult::ErrorIndexingType(
self.map_type_name(val.type_name()).to_string(),
idx_pos,
op_pos,
))
}
}
@ -517,16 +556,8 @@ impl Engine<'_> {
scope: &mut Scope,
lhs: &'a Expr,
idx_expr: &Expr,
idx_pos: Position,
) -> Result<
(
IndexSourceType,
Option<(&'a str, VariableType, usize)>,
usize,
Dynamic,
),
EvalAltResult,
> {
op_pos: Position,
) -> Result<(IndexSourceType, Option<ScopeSource<'a>>, usize, Dynamic), EvalAltResult> {
let idx = self.eval_index_value(scope, idx_expr)?;
match lhs {
@ -534,13 +565,17 @@ impl Engine<'_> {
Expr::Variable(id, _) => Self::search_scope(
scope,
&id,
|val| self.get_indexed_value(&val, idx, idx_expr.position(), idx_pos),
|val| self.get_indexed_value(&val, idx, idx_expr.position(), op_pos),
lhs.position(),
)
.map(|(src_idx, var_type, (val, src_type))| {
.map(|(src, (val, src_type))| {
(
src_type,
Some((id.as_str(), var_type, src_idx)),
Some(ScopeSource {
name: &id,
var_type: src.var_type,
idx: src.idx,
}),
idx as usize,
val,
)
@ -550,7 +585,7 @@ impl Engine<'_> {
expr => {
let val = self.eval_expr(scope, expr)?;
self.get_indexed_value(&val, idx, idx_expr.position(), idx_pos)
self.get_indexed_value(&val, idx, idx_expr.position(), op_pos)
.map(|(v, _)| (IndexSourceType::Expression, None, idx as usize, v))
}
}
@ -575,26 +610,26 @@ impl Engine<'_> {
fn update_indexed_var_in_scope(
src_type: IndexSourceType,
scope: &mut Scope,
id: &str,
src_idx: usize,
src: ScopeSource,
idx: usize,
new_val: Dynamic,
val_pos: Position,
new_val: (Dynamic, Position),
) -> Result<Dynamic, EvalAltResult> {
match src_type {
// array_id[idx] = val
IndexSourceType::Array => {
let arr = scope.get_mut_by_type::<Array>(id, src_idx);
Ok((arr[idx as usize] = new_val).into_dynamic())
let arr = scope.get_mut_by_type::<Array>(src.name, src.idx);
Ok((arr[idx as usize] = new_val.0).into_dynamic())
}
// string_id[idx] = val
IndexSourceType::String => {
let s = scope.get_mut_by_type::<String>(id, src_idx);
let s = scope.get_mut_by_type::<String>(src.name, src.idx);
let pos = new_val.1;
// Value must be a character
let ch = *new_val
.0
.downcast::<char>()
.map_err(|_| EvalAltResult::ErrorCharMismatch(val_pos))?;
.map_err(|_| EvalAltResult::ErrorCharMismatch(pos))?;
Ok(Self::str_replace_char(s, idx as usize, ch).into_dynamic())
}
@ -634,21 +669,20 @@ impl Engine<'_> {
scope: &mut Scope,
this_ptr: &mut Variant,
dot_rhs: &Expr,
mut new_val: Dynamic,
val_pos: Position,
new_val: (&mut Dynamic, Position),
) -> Result<Dynamic, EvalAltResult> {
match dot_rhs {
// xxx.id
Expr::Property(id, pos) => {
let set_fn_name = format!("{}{}", FUNC_SETTER, id);
self.call_fn_raw(&set_fn_name, vec![this_ptr, new_val.as_mut()], None, *pos)
self.call_fn_raw(&set_fn_name, vec![this_ptr, new_val.0.as_mut()], None, *pos)
}
// xxx.lhs[idx_expr]
// TODO - Allow chaining of indexing!
#[cfg(not(feature = "no_index"))]
Expr::Index(lhs, idx_expr, idx_pos) => match lhs.as_ref() {
Expr::Index(lhs, idx_expr, op_pos) => match lhs.as_ref() {
// xxx.id[idx_expr]
Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
@ -656,7 +690,12 @@ impl Engine<'_> {
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
.and_then(|v| {
let idx = self.eval_index_value(scope, idx_expr)?;
Self::update_indexed_value(v, idx as usize, new_val, val_pos)
Self::update_indexed_value(
v,
idx as usize,
new_val.0.clone(),
new_val.1,
)
})
.and_then(|mut v| {
let set_fn_name = format!("{}{}", FUNC_SETTER, id);
@ -667,7 +706,7 @@ impl Engine<'_> {
// All others - syntax error for setters chain
_ => Err(EvalAltResult::ErrorDotExpr(
"for assignment".to_string(),
*idx_pos,
*op_pos,
)),
},
@ -679,7 +718,7 @@ impl Engine<'_> {
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
.and_then(|mut v| {
self.set_dot_val_helper(scope, v.as_mut(), rhs, new_val, val_pos)
self.set_dot_val_helper(scope, v.as_mut(), rhs, new_val)
.map(|_| v) // Discard Ok return value
})
.and_then(|mut v| {
@ -692,7 +731,7 @@ impl Engine<'_> {
// xxx.lhs[idx_expr].rhs
// TODO - Allow chaining of indexing!
#[cfg(not(feature = "no_index"))]
Expr::Index(lhs, idx_expr, idx_pos) => match lhs.as_ref() {
Expr::Index(lhs, idx_expr, op_pos) => match lhs.as_ref() {
// xxx.id[idx_expr].rhs
Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
@ -701,15 +740,10 @@ impl Engine<'_> {
.and_then(|v| {
let idx = self.eval_index_value(scope, idx_expr)?;
let (mut target, _) =
self.get_indexed_value(&v, idx, idx_expr.position(), *idx_pos)?;
self.get_indexed_value(&v, idx, idx_expr.position(), *op_pos)?;
self.set_dot_val_helper(
scope,
target.as_mut(),
rhs,
new_val,
val_pos,
)?;
let val_pos = new_val.1;
self.set_dot_val_helper(scope, target.as_mut(), rhs, new_val)?;
// In case the expression mutated `target`, we need to update it back into the scope because it is cloned.
Self::update_indexed_value(v, idx as usize, target, val_pos)
@ -729,7 +763,7 @@ impl Engine<'_> {
// All others - syntax error for setters chain
_ => Err(EvalAltResult::ErrorDotExpr(
"for assignment".to_string(),
*idx_pos,
*op_pos,
)),
},
@ -754,14 +788,14 @@ impl Engine<'_> {
scope: &mut Scope,
dot_lhs: &Expr,
dot_rhs: &Expr,
new_val: Dynamic,
val_pos: Position,
new_val: (&mut Dynamic, Position),
op_pos: Position,
) -> Result<Dynamic, EvalAltResult> {
match dot_lhs {
// id.???
Expr::Variable(id, pos) => {
let (src_idx, var_type, mut target) = Self::search_scope(scope, id, Ok, *pos)?;
let (ScopeSource { idx, var_type, .. }, mut target) =
Self::search_scope(scope, id, Ok, *pos)?;
match var_type {
VariableType::Constant => {
@ -773,11 +807,10 @@ impl Engine<'_> {
_ => (),
}
let val =
self.set_dot_val_helper(scope, target.as_mut(), dot_rhs, new_val, val_pos);
let val = self.set_dot_val_helper(scope, target.as_mut(), dot_rhs, new_val);
// In case the expression mutated `target`, we need to update it back into the scope because it is cloned.
*scope.get_mut(id, src_idx) = target;
*scope.get_mut(id, idx) = target;
val
}
@ -785,24 +818,28 @@ impl Engine<'_> {
// lhs[idx_expr].???
// TODO - Allow chaining of indexing!
#[cfg(not(feature = "no_index"))]
Expr::Index(lhs, idx_expr, idx_pos) => {
Expr::Index(lhs, idx_expr, op_pos) => {
let (src_type, src, idx, mut target) =
self.eval_index_expr(scope, lhs, idx_expr, *idx_pos)?;
let val =
self.set_dot_val_helper(scope, target.as_mut(), dot_rhs, new_val, val_pos);
self.eval_index_expr(scope, lhs, idx_expr, *op_pos)?;
let val_pos = new_val.1;
let val = self.set_dot_val_helper(scope, target.as_mut(), dot_rhs, new_val);
// In case the expression mutated `target`, we need to update it back into the scope because it is cloned.
if let Some((id, var_type, src_idx)) = src {
match var_type {
if let Some(src) = src {
match src.var_type {
VariableType::Constant => {
return Err(EvalAltResult::ErrorAssignmentToConstant(
id.to_string(),
src.name.to_string(),
lhs.position(),
));
}
VariableType::Normal => {
Self::update_indexed_var_in_scope(
src_type, scope, id, src_idx, idx, target, val_pos,
src_type,
scope,
src,
idx,
(target, val_pos),
)?;
}
}
@ -828,15 +865,13 @@ impl Engine<'_> {
Expr::IntegerConstant(i, _) => Ok(i.into_dynamic()),
Expr::StringConstant(s, _) => Ok(s.into_dynamic()),
Expr::CharConstant(c, _) => Ok(c.into_dynamic()),
Expr::Variable(id, pos) => {
Self::search_scope(scope, id, Ok, *pos).map(|(_, _, val)| val)
}
Expr::Variable(id, pos) => Self::search_scope(scope, id, Ok, *pos).map(|(_, val)| val),
Expr::Property(_, _) => panic!("unexpected property."),
// lhs[idx_expr]
#[cfg(not(feature = "no_index"))]
Expr::Index(lhs, idx_expr, idx_pos) => self
.eval_index_expr(scope, lhs, idx_expr, *idx_pos)
Expr::Index(lhs, idx_expr, op_pos) => self
.eval_index_expr(scope, lhs, idx_expr, *op_pos)
.map(|(_, _, _, x)| x),
// Statement block
@ -844,43 +879,55 @@ impl Engine<'_> {
// lhs = rhs
Expr::Assignment(lhs, rhs, op_pos) => {
let rhs_val = self.eval_expr(scope, rhs)?;
let mut rhs_val = self.eval_expr(scope, rhs)?;
match lhs.as_ref() {
// name = rhs
Expr::Variable(name, pos) => match scope.get(name) {
Some((idx, _, VariableType::Normal, _)) => {
Some((
ScopeSource {
idx,
var_type: VariableType::Normal,
..
},
_,
)) => {
*scope.get_mut(name, idx) = rhs_val.clone();
Ok(rhs_val)
}
Some((_, _, VariableType::Constant, _)) => Err(
EvalAltResult::ErrorAssignmentToConstant(name.to_string(), *op_pos),
),
Some((
ScopeSource {
var_type: VariableType::Constant,
..
},
_,
)) => Err(EvalAltResult::ErrorAssignmentToConstant(
name.to_string(),
*op_pos,
)),
_ => Err(EvalAltResult::ErrorVariableNotFound(name.clone(), *pos)),
},
// idx_lhs[idx_expr] = rhs
#[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, idx_expr, idx_pos) => {
Expr::Index(idx_lhs, idx_expr, op_pos) => {
let (src_type, src, idx, _) =
self.eval_index_expr(scope, idx_lhs, idx_expr, *idx_pos)?;
self.eval_index_expr(scope, idx_lhs, idx_expr, *op_pos)?;
if let Some((id, var_type, src_idx)) = src {
match var_type {
if let Some(src) = src {
match src.var_type {
VariableType::Constant => {
return Err(EvalAltResult::ErrorAssignmentToConstant(
id.to_string(),
src.name.to_string(),
idx_lhs.position(),
));
}
VariableType::Normal => Ok(Self::update_indexed_var_in_scope(
src_type,
scope,
&id,
src_idx,
src,
idx,
rhs_val,
rhs.position(),
(rhs_val, rhs.position()),
)?),
}
} else {
@ -891,9 +938,13 @@ impl Engine<'_> {
}
// dot_lhs.dot_rhs = rhs
Expr::Dot(dot_lhs, dot_rhs, _) => {
self.set_dot_val(scope, dot_lhs, dot_rhs, rhs_val, rhs.position(), *op_pos)
}
Expr::Dot(dot_lhs, dot_rhs, _) => self.set_dot_val(
scope,
dot_lhs,
dot_rhs,
(&mut rhs_val, rhs.position()),
*op_pos,
),
// Error assignment to constant
expr if expr.is_constant() => Err(EvalAltResult::ErrorAssignmentToConstant(
@ -919,8 +970,24 @@ impl Engine<'_> {
Ok(Box::new(arr))
}
Expr::FunctionCall(fn_name, args_expr_list, def_val, pos) => {
// Has a system function an override?
fn has_override(engine: &Engine, name: &str) -> bool {
let spec = FnSpec {
name: name.into(),
args: Some(vec![TypeId::of::<String>()]),
};
engine.ext_functions.contains_key(&spec)
|| engine
.script_functions
.binary_search_by(|f| f.compare(name, 1))
.is_ok()
}
match fn_name.as_str() {
// Dump AST
Expr::FunctionCall(fn_name, args_expr_list, _, pos) if fn_name == KEYWORD_DUMP_AST => {
KEYWORD_DUMP_AST => {
let pos = if args_expr_list.len() == 0 {
*pos
} else {
@ -943,8 +1010,58 @@ impl Engine<'_> {
)
}
// type_of
KEYWORD_TYPE_OF
if args_expr_list.len() == 1 && !has_override(self, KEYWORD_TYPE_OF) =>
{
let r = self.eval_expr(scope, &args_expr_list[0])?;
Ok(self
.map_type_name((*r).type_name())
.to_string()
.into_dynamic())
}
// eval
KEYWORD_EVAL
if args_expr_list.len() == 1 && !has_override(self, KEYWORD_EVAL) =>
{
let pos = args_expr_list[0].position();
let r = self.eval_expr(scope, &args_expr_list[0])?;
let script =
r.downcast_ref::<String>()
.map(String::as_str)
.ok_or_else(|| {
EvalAltResult::ErrorMismatchOutputType(
r.type_name().into(),
pos,
)
})?;
#[cfg(not(feature = "no_optimize"))]
let ast = {
let orig_optimization_level = self.optimization_level;
self.set_optimization_level(OptimizationLevel::None);
let ast = self.compile(script);
self.set_optimization_level(orig_optimization_level);
ast.map_err(EvalAltResult::ErrorParsing)?
};
#[cfg(feature = "no_optimize")]
let ast = self.compile(script).map_err(EvalAltResult::ErrorParsing)?;
return Ok(self.eval_ast_with_scope_raw(scope, true, &ast).map_err(
|mut err| {
err.set_position(pos);
err
},
)?);
}
// Normal function call
Expr::FunctionCall(fn_name, args_expr_list, def_val, pos) => {
_ => {
let mut values = args_expr_list
.iter()
.map(|expr| self.eval_expr(scope, expr))
@ -957,6 +1074,8 @@ impl Engine<'_> {
*pos,
)
}
}
}
Expr::And(lhs, rhs) => Ok(Box::new(
*self
@ -1040,7 +1159,7 @@ impl Engine<'_> {
Stmt::IfElse(guard, if_body, else_body) => self
.eval_expr(scope, guard)?
.downcast::<bool>()
.map_err(|_| EvalAltResult::ErrorIfGuard(guard.position()))
.map_err(|_| EvalAltResult::ErrorLogicGuard(guard.position()))
.and_then(|guard_val| {
if *guard_val {
self.eval_stmt(scope, if_body)
@ -1067,7 +1186,7 @@ impl Engine<'_> {
return Ok(().into_dynamic());
}
}
Err(_) => return Err(EvalAltResult::ErrorIfGuard(guard.position())),
Err(_) => return Err(EvalAltResult::ErrorLogicGuard(guard.position())),
}
},

View File

@ -47,7 +47,7 @@ pub enum ParseErrorType {
/// Error in the script text. Wrapped value is the error message.
BadInput(String),
/// The script ends prematurely.
InputPastEndOfFile,
UnexpectedEOF,
/// An unknown operator is encountered. Wrapped value is the operator.
UnknownOperator(String),
/// An open `(` is missing the corresponding closing `)`.
@ -72,6 +72,8 @@ pub enum ParseErrorType {
ForbiddenConstantExpr(String),
/// Missing a variable name after the `let`, `const` or `for` keywords.
VariableExpected,
/// Missing an expression.
ExprExpected(String),
/// A `for` statement is missing the `in` keyword.
MissingIn,
/// Defining a function `fn` in an appropriate place (e.g. inside another function).
@ -119,7 +121,7 @@ impl ParseError {
pub(crate) fn desc(&self) -> &str {
match self.0 {
ParseErrorType::BadInput(ref p) => p,
ParseErrorType::InputPastEndOfFile => "Script is incomplete",
ParseErrorType::UnexpectedEOF => "Script is incomplete",
ParseErrorType::UnknownOperator(_) => "Unknown operator",
ParseErrorType::MissingRightParen(_) => "Expecting ')'",
ParseErrorType::MissingLeftBrace => "Expecting '{'",
@ -134,6 +136,7 @@ impl ParseError {
ParseErrorType::ForbiddenConstantExpr(_) => "Expecting a constant",
ParseErrorType::MissingIn => "Expecting 'in'",
ParseErrorType::VariableExpected => "Expecting name of a variable",
ParseErrorType::ExprExpected(_) => "Expecting an expression",
#[cfg(not(feature = "no_function"))]
ParseErrorType::FnMissingName => "Expecting name in function declaration",
#[cfg(not(feature = "no_function"))]
@ -168,6 +171,8 @@ impl fmt::Display for ParseError {
write!(f, "{}", if s.is_empty() { self.desc() } else { s })?
}
ParseErrorType::ExprExpected(ref s) => write!(f, "Expecting {} expression", s)?,
#[cfg(not(feature = "no_function"))]
ParseErrorType::FnMissingParams(ref s) => {
write!(f, "Expecting parameters for function '{}'", s)?

View File

@ -2,7 +2,8 @@
use crate::any::{Any, Dynamic};
use crate::engine::{
Engine, FnCallArgs, KEYWORD_DEBUG, KEYWORD_DUMP_AST, KEYWORD_PRINT, KEYWORD_TYPE_OF,
Engine, FnCallArgs, KEYWORD_DEBUG, KEYWORD_DUMP_AST, KEYWORD_EVAL, KEYWORD_PRINT,
KEYWORD_TYPE_OF,
};
use crate::parser::{map_dynamic_to_expr, Expr, FnDef, ReturnType, Stmt, AST};
use crate::scope::{Scope, ScopeEntry, VariableType};
@ -287,7 +288,7 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) -
/// Optimize an expression.
fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr {
// These keywords are handled specially
const SKIP_FUNC_KEYWORDS: [&str; 3] = [KEYWORD_PRINT, KEYWORD_DEBUG, KEYWORD_DUMP_AST];
const DONT_EVAL_KEYWORDS: [&str; 3] = [KEYWORD_PRINT, KEYWORD_DEBUG, KEYWORD_EVAL];
match expr {
// ( stmt )
@ -431,15 +432,25 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr {
),
},
// Do not optimize anything within built-in function keywords
Expr::FunctionCall(id, args, def_value, pos) if SKIP_FUNC_KEYWORDS.contains(&id.as_str())=>
// Do not optimize anything within dump_ast
Expr::FunctionCall(id, args, def_value, pos) if id == KEYWORD_DUMP_AST =>
Expr::FunctionCall(id, args, def_value, pos),
// Do not optimize anything within built-in function keywords
Expr::FunctionCall(id, args, def_value, pos) if DONT_EVAL_KEYWORDS.contains(&id.as_str())=>
Expr::FunctionCall(id, args.into_iter().map(|a| optimize_expr(a, state)).collect(), def_value, pos),
// Eagerly call functions
Expr::FunctionCall(id, args, def_value, pos)
if state.engine.optimization_level == OptimizationLevel::Full // full optimizations
&& args.iter().all(|expr| expr.is_constant()) // all arguments are constants
=> {
// First search in script-defined functions (can override built-in)
if state.engine.script_functions.binary_search_by(|f| f.compare(&id, args.len())).is_ok() {
// A script-defined function overrides the built-in function - do not make the call
return Expr::FunctionCall(id, args.into_iter().map(|a| optimize_expr(a, state)).collect(), def_value, pos);
}
let mut arg_values: Vec<_> = args.iter().map(Expr::get_constant_value).collect();
let call_args: FnCallArgs = arg_values.iter_mut().map(Dynamic::as_mut).collect();
@ -467,19 +478,19 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr {
})
).flatten().unwrap_or_else(|| Expr::FunctionCall(id, args, def_value, pos))
}
// id(args ..) -> optimize function call arguments
Expr::FunctionCall(id, args, def_value, pos) =>
Expr::FunctionCall(id, args.into_iter().map(|a| optimize_expr(a, state)).collect(), def_value, pos),
// constant-name
Expr::Variable(ref name, _) if state.contains_constant(name) => {
state.set_dirty();
// Replace constant with value
state
.find_constant(name)
.expect("should find constant in scope!")
.clone()
state.find_constant(name).expect("should find constant in scope!").clone()
}
// All other expressions - skip
expr => expr,
}

View File

@ -469,6 +469,10 @@ pub enum Token {
UnaryMinus,
Multiply,
Divide,
Modulo,
PowerOf,
LeftShift,
RightShift,
SemiColon,
Colon,
Comma,
@ -482,15 +486,18 @@ pub enum Token {
Else,
While,
Loop,
For,
In,
LessThan,
GreaterThan,
Bang,
LessThanEqualsTo,
GreaterThanEqualsTo,
EqualsTo,
NotEqualsTo,
Bang,
Pipe,
Or,
XOr,
Ampersand,
And,
#[cfg(not(feature = "no_function"))]
@ -507,15 +514,8 @@ pub enum Token {
AndAssign,
OrAssign,
XOrAssign,
LeftShift,
RightShift,
XOr,
Modulo,
ModuloAssign,
PowerOf,
PowerOfAssign,
For,
In,
LexError(LexError),
}
@ -1601,23 +1601,21 @@ fn parse_array_literal<'a>(
/// Parse a primary expression.
fn parse_primary<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, ParseError> {
let token = match input
.peek()
.ok_or_else(|| ParseError::new(PERR::UnexpectedEOF, Position::eof()))?
{
// { - block statement as expression
match input.peek() {
Some((Token::LeftBrace, pos)) => {
(Token::LeftBrace, pos) => {
let pos = *pos;
return parse_block(input, false).map(|block| Expr::Stmt(Box::new(block), pos));
}
_ => (),
}
let token = input.next();
_ => input.next().expect("should be a token"),
};
let mut can_be_indexed = false;
#[allow(unused_mut)]
let mut root_expr = match token
.ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))?
{
let mut root_expr = match token {
#[cfg(not(feature = "no_float"))]
(Token::FloatConstant(x), pos) => Ok(Expr::FloatConstant(x, pos)),
@ -1666,7 +1664,7 @@ fn parse_primary<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, Pa
fn parse_unary<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, ParseError> {
match input
.peek()
.ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))?
.ok_or_else(|| ParseError::new(PERR::UnexpectedEOF, Position::eof()))?
{
// -expr
(Token::UnaryMinus, pos) => {
@ -1978,6 +1976,22 @@ fn parse_expr<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, Parse
parse_binary_op(input, 1, lhs)
}
/// Make sure that the expression is not a statement expression (i.e. wrapped in {})
fn ensure_not_statement_expr<'a>(
input: &mut Peekable<TokenIterator<'a>>,
type_name: &str,
) -> Result<(), ParseError> {
match input
.peek()
.ok_or_else(|| ParseError(PERR::ExprExpected(type_name.to_string()), Position::eof()))?
{
// Disallow statement expressions
(Token::LeftBrace, pos) => Err(ParseError(PERR::ExprExpected(type_name.to_string()), *pos)),
// No need to check for others at this time - leave it for the expr parser
_ => Ok(()),
}
}
/// Parse an if statement.
fn parse_if<'a>(
input: &mut Peekable<TokenIterator<'a>>,
@ -1986,19 +2000,20 @@ fn parse_if<'a>(
// if ...
input.next();
// if guard { body }
// if guard { if_body }
ensure_not_statement_expr(input, "a boolean")?;
let guard = parse_expr(input)?;
let if_body = parse_block(input, breakable)?;
// if guard { body } else ...
// if guard { if_body } else ...
let else_body = if matches!(input.peek(), Some((Token::Else, _))) {
input.next();
Some(Box::new(if matches!(input.peek(), Some((Token::If, _))) {
// if guard { body } else if ...
// if guard { if_body } else if ...
parse_if(input, breakable)?
} else {
// if guard { body } else { else-body }
// if guard { if_body } else { else-body }
parse_block(input, breakable)?
}))
} else {
@ -2014,6 +2029,7 @@ fn parse_while<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, Pars
input.next();
// while guard { body }
ensure_not_statement_expr(input, "a boolean")?;
let guard = parse_expr(input)?;
let body = parse_block(input, true)?;
@ -2061,6 +2077,7 @@ fn parse_for<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseE
}
// for name in expr { body }
ensure_not_statement_expr(input, "a boolean")?;
let expr = parse_expr(input)?;
let body = parse_block(input, true)?;
@ -2189,10 +2206,12 @@ fn parse_stmt<'a>(
input: &mut Peekable<TokenIterator<'a>>,
breakable: bool,
) -> Result<Stmt, ParseError> {
match input
.peek()
.ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))?
{
let token = match input.peek() {
Some(token) => token,
None => return Ok(Stmt::Noop(Position::eof())),
};
match token {
// Semicolon - empty statement
(Token::SemiColon, pos) => Ok(Stmt::Noop(*pos)),
@ -2210,31 +2229,27 @@ fn parse_stmt<'a>(
Ok(Stmt::Break(pos))
}
(Token::Break, pos) => return Err(ParseError::new(PERR::LoopBreak, *pos)),
(token @ Token::Return, _) | (token @ Token::Throw, _) => {
(token @ Token::Return, pos) | (token @ Token::Throw, pos) => {
let return_type = match token {
Token::Return => ReturnType::Return,
Token::Throw => ReturnType::Exception,
_ => panic!("token should be return or throw"),
};
let pos = *pos;
input.next();
match input.peek() {
// `return`/`throw` at EOF
None => Ok(Stmt::ReturnWithVal(None, return_type, Position::eof())),
// `return;` or `throw;`
Some((Token::SemiColon, pos)) => {
let pos = *pos;
Ok(Stmt::ReturnWithVal(None, return_type, pos))
}
Some((Token::SemiColon, _)) => Ok(Stmt::ReturnWithVal(None, return_type, pos)),
// `return` or `throw` with expression
Some((_, pos)) => {
let pos = *pos;
Ok(Stmt::ReturnWithVal(
Some(Box::new(parse_expr(input)?)),
return_type,
pos,
))
Some((_, _)) => {
let expr = parse_expr(input)?;
let pos = expr.position();
Ok(Stmt::ReturnWithVal(Some(Box::new(expr)), return_type, pos))
}
}
}
@ -2248,10 +2263,7 @@ fn parse_stmt<'a>(
/// Parse a function definition.
#[cfg(not(feature = "no_function"))]
fn parse_fn<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<FnDef, ParseError> {
let pos = input
.next()
.ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))?
.1;
let pos = input.next().expect("should be fn").1;
let name = match input
.next()

View File

@ -40,8 +40,8 @@ pub enum EvalAltResult {
ErrorIndexingType(String, Position),
/// Trying to index into an array or string with an index that is not `i64`.
ErrorIndexExpr(Position),
/// The guard expression in an `if` statement does not return a boolean value.
ErrorIfGuard(Position),
/// The guard expression in an `if` or `while` statement does not return a boolean value.
ErrorLogicGuard(Position),
/// The `for` statement encounters a type that is not an iterator.
ErrorFor(Position),
/// Usage of an unknown variable. Wrapped value is the name of the variable.
@ -93,7 +93,7 @@ impl EvalAltResult {
}
Self::ErrorStringBounds(0, _, _) => "Indexing of empty string",
Self::ErrorStringBounds(_, _, _) => "String index out of bounds",
Self::ErrorIfGuard(_) => "If guard expects boolean expression",
Self::ErrorLogicGuard(_) => "Boolean expression expected",
Self::ErrorFor(_) => "For loop expects array or range",
Self::ErrorVariableNotFound(_, _) => "Variable not found",
Self::ErrorAssignmentToUnknownLHS(_) => {
@ -123,7 +123,7 @@ impl fmt::Display for EvalAltResult {
Self::ErrorVariableNotFound(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos),
Self::ErrorIndexingType(_, pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorIndexExpr(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorIfGuard(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorLogicGuard(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorFor(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorAssignmentToUnknownLHS(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorAssignmentToConstant(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos),
@ -222,7 +222,7 @@ impl EvalAltResult {
| Self::ErrorStringBounds(_, _, pos)
| Self::ErrorIndexingType(_, pos)
| Self::ErrorIndexExpr(pos)
| Self::ErrorIfGuard(pos)
| Self::ErrorLogicGuard(pos)
| Self::ErrorFor(pos)
| Self::ErrorVariableNotFound(_, pos)
| Self::ErrorAssignmentToUnknownLHS(pos)
@ -250,7 +250,7 @@ impl EvalAltResult {
| Self::ErrorStringBounds(_, _, ref mut pos)
| Self::ErrorIndexingType(_, ref mut pos)
| Self::ErrorIndexExpr(ref mut pos)
| Self::ErrorIfGuard(ref mut pos)
| Self::ErrorLogicGuard(ref mut pos)
| Self::ErrorFor(ref mut pos)
| Self::ErrorVariableNotFound(_, ref mut pos)
| Self::ErrorAssignmentToUnknownLHS(ref mut pos)

View File

@ -31,6 +31,13 @@ pub struct ScopeEntry<'a> {
pub expr: Option<Expr>,
}
/// Information about a particular entry in the Scope.
pub(crate) struct ScopeSource<'a> {
pub name: &'a str,
pub idx: usize,
pub var_type: VariableType,
}
/// A type containing information about the current scope.
/// Useful for keeping state between `Engine` evaluation runs.
///
@ -71,7 +78,7 @@ impl<'a> Scope<'a> {
}
/// Add (push) a new variable to the Scope.
pub fn push<K: Into<Cow<'a, str>>, T: Any>(&mut self, name: K, value: T) {
pub fn push<K: Into<Cow<'a, str>>, T: Any + Clone>(&mut self, name: K, value: T) {
let value = value.into_dynamic();
// Map into constant expressions
@ -91,7 +98,7 @@ impl<'a> Scope<'a> {
/// Constants propagation is a technique used to optimize an AST.
/// However, in order to be used for optimization, constants must be in one of the recognized types:
/// `INT` (default to `i64`, `i32` if `only_i32`), `f64`, `String`, `char` and `bool`.
pub fn push_constant<K: Into<Cow<'a, str>>, T: Any>(&mut self, name: K, value: T) {
pub fn push_constant<K: Into<Cow<'a, str>>, T: Any + Clone>(&mut self, name: K, value: T) {
let value = value.into_dynamic();
// Map into constant expressions
@ -139,8 +146,18 @@ impl<'a> Scope<'a> {
self.0.truncate(size);
}
/// Does the scope contain the variable?
pub fn contains(&self, key: &str) -> bool {
self.0
.iter()
.enumerate()
.rev() // Always search a Scope in reverse order
.find(|(_, ScopeEntry { name, .. })| name == key)
.is_some()
}
/// Find a variable in the Scope, starting from the last.
pub fn get(&self, key: &str) -> Option<(usize, &str, VariableType, Dynamic)> {
pub(crate) fn get(&self, key: &str) -> Option<(ScopeSource, Dynamic)> {
self.0
.iter()
.enumerate()
@ -155,7 +172,16 @@ impl<'a> Scope<'a> {
value,
..
},
)| (i, name.as_ref(), *var_type, value.clone()),
)| {
(
ScopeSource {
name: name.as_ref(),
idx: i,
var_type: *var_type,
},
value.clone(),
)
},
)
}
@ -174,11 +200,11 @@ impl<'a> Scope<'a> {
pub(crate) fn get_mut(&mut self, name: &str, index: usize) -> &mut Dynamic {
let entry = self.0.get_mut(index).expect("invalid index in Scope");
assert_ne!(
entry.var_type,
VariableType::Constant,
"get mut of constant variable"
);
// assert_ne!(
// entry.var_type,
// VariableType::Constant,
// "get mut of constant variable"
// );
assert_eq!(entry.name, name, "incorrect key at Scope entry");
&mut entry.value

79
tests/eval.rs Normal file
View File

@ -0,0 +1,79 @@
use rhai::{Engine, EvalAltResult, Scope, INT};
#[test]
fn test_eval() -> Result<(), EvalAltResult> {
let mut engine = Engine::new();
assert_eq!(
engine.eval::<INT>(
r#"
eval("40 + 2")
"#
)?,
42
);
Ok(())
}
#[test]
#[cfg(not(feature = "no_function"))]
fn test_eval_function() -> Result<(), EvalAltResult> {
let mut engine = Engine::new();
let mut scope = Scope::new();
assert_eq!(
engine.eval_with_scope::<INT>(
&mut scope,
r#"
let x = 10;
fn foo(x) { x += 12; x }
let script = "let y = x;"; // build a script
script += "y += foo(y);";
script += "x + y";
eval(script)
"#
)?,
42
);
assert_eq!(
scope
.get_value::<INT>("x")
.expect("variable x should exist"),
10
);
assert_eq!(
scope
.get_value::<INT>("y")
.expect("variable y should exist"),
32
);
assert!(!scope.contains("z"));
Ok(())
}
#[test]
#[cfg(not(feature = "no_function"))]
fn test_eval_override() -> Result<(), EvalAltResult> {
let mut engine = Engine::new();
assert_eq!(
engine.eval::<String>(
r#"
fn eval(x) { x } // reflect the script back
eval("40 + 2")
"#
)?,
"40 + 2"
);
Ok(())
}

View File

@ -91,7 +91,7 @@ fn test_big_get_set() -> Result<(), EvalAltResult> {
);
assert_eq!(
engine.eval::<String>("let a = new_tp(); a.type_of()")?,
engine.eval::<String>("let a = new_tp(); type_of(a)")?,
"TestParent"
);

39
tests/side_effects.rs Normal file
View File

@ -0,0 +1,39 @@
use rhai::{Engine, EvalAltResult, RegisterFn, Scope};
use std::cell::Cell;
use std::rc::Rc;
#[derive(Debug, Clone)]
struct CommandWrapper {
value: Rc<Cell<i64>>,
}
impl CommandWrapper {
pub fn set_value(&mut self, x: i64) {
let val = self.value.get();
self.value.set(val + x);
}
}
#[test]
fn test_side_effects() -> Result<(), EvalAltResult> {
let mut engine = Engine::new();
let mut scope = Scope::new();
let payload = Rc::new(Cell::new(12));
assert_eq!(payload.get(), 12);
let command = CommandWrapper {
value: payload.clone(),
};
scope.push_constant("Command", command);
engine.register_type_with_name::<CommandWrapper>("CommandType");
engine.register_fn("action", CommandWrapper::set_value);
engine.eval_with_scope::<()>(&mut scope, "Command.action(30)")?;
assert_eq!(payload.get(), 42);
Ok(())
}

View File

@ -23,10 +23,10 @@ fn test_type_of() -> Result<(), EvalAltResult> {
assert_eq!(engine.eval::<String>(r#"type_of("hello")"#)?, "string");
#[cfg(not(feature = "only_i32"))]
assert_eq!(engine.eval::<String>("let x = 123; x.type_of()")?, "i64");
assert_eq!(engine.eval::<String>("let x = 123; type_of(x)")?, "i64");
#[cfg(feature = "only_i32")]
assert_eq!(engine.eval::<String>("let x = 123; x.type_of()")?, "i32");
assert_eq!(engine.eval::<String>("let x = 123; type_of(x)")?, "i32");
Ok(())
}

View File

@ -42,10 +42,7 @@ fn test_scope_eval() -> Result<(), EvalAltResult> {
assert_eq!(
scope
.get_value::<INT>("y")
.ok_or(EvalAltResult::ErrorRuntime(
"variable y not found".into(),
Default::default()
))?,
.expect("variable y should exist"),
1
);