commit
9c7fa97171
134
README.md
134
README.md
@ -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);
|
||||
```
|
||||
|
104
src/api.rs
104
src/api.rs
@ -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).
|
||||
|
407
src/engine.rs
407
src/engine.rs
@ -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())),
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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)?
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
44
src/scope.rs
44
src/scope.rs
@ -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
79
tests/eval.rs
Normal 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(())
|
||||
}
|
@ -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
39
tests/side_effects.rs
Normal 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(())
|
||||
}
|
@ -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(())
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user