Add progress tracking and operations limit.
This commit is contained in:
parent
5d5ceb4049
commit
55c97eb649
150
README.md
150
README.md
@ -21,11 +21,15 @@ Rhai's current features set:
|
|||||||
* Fairly efficient (1 million iterations in 0.75 sec on my 5 year old laptop)
|
* Fairly efficient (1 million iterations in 0.75 sec on my 5 year old laptop)
|
||||||
* Low compile-time overhead (~0.6 sec debug/~3 sec release for script runner app)
|
* Low compile-time overhead (~0.6 sec debug/~3 sec release for script runner app)
|
||||||
* Relatively little `unsafe` code (yes there are some for performance reasons)
|
* Relatively little `unsafe` code (yes there are some for performance reasons)
|
||||||
|
* Sand-boxed (the scripting [`Engine`] can be declared immutable which cannot mutate the containing environment
|
||||||
|
unless explicitly allowed via `RefCell` etc.)
|
||||||
|
* Rugged (protection against [stack-overflow](#maximum-stack-depth) and [runaway scripts](#maximum-number-of-operations))
|
||||||
|
* Able to set limits on script resource usage (e.g. see [tracking progress](#tracking-progress))
|
||||||
* [`no-std`](#optional-features) support
|
* [`no-std`](#optional-features) support
|
||||||
* [Function overloading](#function-overloading)
|
* [Function overloading](#function-overloading)
|
||||||
* [Operator overloading](#operator-overloading)
|
* [Operator overloading](#operator-overloading)
|
||||||
* [Modules]
|
* [Modules]
|
||||||
* Compiled script is [optimized](#script-optimization) for repeat evaluations
|
* Compiled script is [optimized](#script-optimization) for repeated evaluations
|
||||||
* Support for [minimal builds](#minimal-builds) by excluding unneeded language [features](#optional-features)
|
* Support for [minimal builds](#minimal-builds) by excluding unneeded language [features](#optional-features)
|
||||||
* Very few additional dependencies (right now only [`num-traits`](https://crates.io/crates/num-traits/)
|
* Very few additional dependencies (right now only [`num-traits`](https://crates.io/crates/num-traits/)
|
||||||
to do checked arithmetic operations); for [`no-std`](#optional-features) builds, a number of additional dependencies are
|
to do checked arithmetic operations); for [`no-std`](#optional-features) builds, a number of additional dependencies are
|
||||||
@ -63,19 +67,19 @@ Beware that in order to use pre-releases (e.g. alpha and beta), the exact versio
|
|||||||
Optional features
|
Optional features
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
| Feature | Description |
|
| Feature | Description |
|
||||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `unchecked` | Exclude arithmetic checking (such as overflows and division by zero). Beware that a bad script may panic the entire system! |
|
| `unchecked` | Exclude arithmetic checking (such as over-flows and division by zero), stack depth limit and operations count limit. Beware that a bad script may panic the entire system! |
|
||||||
| `no_function` | Disable script-defined functions if not needed. |
|
| `no_function` | Disable script-defined functions if not needed. |
|
||||||
| `no_index` | Disable [arrays] and indexing features if not needed. |
|
| `no_index` | Disable [arrays] and indexing features if not needed. |
|
||||||
| `no_object` | Disable support for custom types and objects. |
|
| `no_object` | Disable support for custom types and objects. |
|
||||||
| `no_float` | Disable floating-point numbers and math if not needed. |
|
| `no_float` | Disable floating-point numbers and math if not needed. |
|
||||||
| `no_optimize` | Disable the script optimizer. |
|
| `no_optimize` | Disable the script optimizer. |
|
||||||
| `no_module` | Disable modules. |
|
| `no_module` | Disable modules. |
|
||||||
| `only_i32` | Set the system integer type to `i32` and disable all other integer types. `INT` is set to `i32`. |
|
| `only_i32` | Set the system integer type to `i32` and disable all other integer types. `INT` is set to `i32`. |
|
||||||
| `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. |
|
| `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. |
|
||||||
| `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. |
|
| `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. |
|
||||||
| `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, [`Engine`], [`Scope`] and `AST` are all `Send + Sync`. |
|
| `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, [`Engine`], [`Scope`] and `AST` are all `Send + Sync`. |
|
||||||
|
|
||||||
By default, Rhai includes all the standard functionalities in a small, tight package.
|
By default, Rhai includes all the standard functionalities in a small, tight package.
|
||||||
Most features are here to opt-**out** of certain functionalities that are not needed.
|
Most features are here to opt-**out** of certain functionalities that are not needed.
|
||||||
@ -269,7 +273,7 @@ let ast = engine.compile_file("hello_world.rhai".into())?;
|
|||||||
|
|
||||||
### Calling Rhai functions from Rust
|
### Calling Rhai functions from Rust
|
||||||
|
|
||||||
Rhai also allows working _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust via `call_fn`.
|
Rhai also allows working _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust via `Engine::call_fn`.
|
||||||
Functions declared with `private` are hidden and cannot be called from Rust (see also [modules]).
|
Functions declared with `private` are hidden and cannot be called from Rust (see also [modules]).
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
@ -372,7 +376,7 @@ Use `Engine::new_raw` to create a _raw_ `Engine`, in which _nothing_ is added, n
|
|||||||
|
|
||||||
### Packages
|
### Packages
|
||||||
|
|
||||||
Rhai functional features are provided in different _packages_ that can be loaded via a call to `load_package`.
|
Rhai functional features are provided in different _packages_ that can be loaded via a call to `Engine::load_package`.
|
||||||
Packages reside under `rhai::packages::*` and the trait `rhai::packages::Package` must be loaded in order for
|
Packages reside under `rhai::packages::*` and the trait `rhai::packages::Package` must be loaded in order for
|
||||||
packages to be used.
|
packages to be used.
|
||||||
|
|
||||||
@ -759,7 +763,7 @@ Because they [_short-circuit_](#boolean-operators), `&&` and `||` are handled sp
|
|||||||
overriding them has no effect at all.
|
overriding them has no effect at all.
|
||||||
|
|
||||||
Operator functions cannot be defined as a script function (because operators syntax are not valid function names).
|
Operator functions cannot be defined as a script function (because operators syntax are not valid function names).
|
||||||
However, operator functions _can_ be registered to the [`Engine`] via `register_fn`, `register_result_fn` etc.
|
However, operator functions _can_ be registered to the [`Engine`] via the methods `Engine::register_fn`, `Engine::register_result_fn` etc.
|
||||||
When a custom operator function is registered with the same name as an operator, it _overloads_ (or overrides) the built-in version.
|
When a custom operator function is registered with the same name as an operator, it _overloads_ (or overrides) the built-in version.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
@ -2043,7 +2047,7 @@ debug("world!"); // prints "world!" to stdout using debug formatting
|
|||||||
### Overriding `print` and `debug` with callback functions
|
### Overriding `print` and `debug` with callback functions
|
||||||
|
|
||||||
When embedding Rhai into an application, it is usually necessary to trap `print` and `debug` output
|
When embedding Rhai into an application, it is usually necessary to trap `print` and `debug` output
|
||||||
(for logging into a tracking log, for example).
|
(for logging into a tracking log, for example) with the `Engine::on_print` and `Engine::on_debug` methods:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Any function or closure that takes an '&str' argument can be used to override
|
// Any function or closure that takes an '&str' argument can be used to override
|
||||||
@ -2237,7 +2241,7 @@ Built-in module resolvers are grouped under the `rhai::module_resolvers` module
|
|||||||
| `FileModuleResolver` | The default module resolution service, not available under the [`no_std`] feature. Loads a script file (based off the current directory) with `.rhai` extension.<br/>The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.<br/>`FileModuleResolver::create_module()` loads a script file and returns a module. |
|
| `FileModuleResolver` | The default module resolution service, not available under the [`no_std`] feature. Loads a script file (based off the current directory) with `.rhai` extension.<br/>The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.<br/>`FileModuleResolver::create_module()` loads a script file and returns a module. |
|
||||||
| `StaticModuleResolver` | Loads modules that are statically added. This can be used when the [`no_std`] feature is turned on. |
|
| `StaticModuleResolver` | Loads modules that are statically added. This can be used when the [`no_std`] feature is turned on. |
|
||||||
|
|
||||||
An [`Engine`]'s module resolver is set via a call to `set_module_resolver`:
|
An [`Engine`]'s module resolver is set via a call to `Engine::set_module_resolver`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Use the 'StaticModuleResolver'
|
// Use the 'StaticModuleResolver'
|
||||||
@ -2248,6 +2252,112 @@ engine.set_module_resolver(Some(resolver));
|
|||||||
engine.set_module_resolver(None);
|
engine.set_module_resolver(None);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Ruggedization - protect against DoS attacks
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
For scripting systems open to user-land scripts, it is always best to limit the amount of resources used by a script
|
||||||
|
so that it does not crash the system by consuming all resources.
|
||||||
|
|
||||||
|
The most important resources to watch out for are:
|
||||||
|
|
||||||
|
* **Memory**: A malignant script may continuously grow an [array] or [object map] until all memory is consumed.
|
||||||
|
* **CPU**: A malignant script may run an infinite tight loop that consumes all CPU cycles.
|
||||||
|
* **Time**: A malignant script may run indefinitely, thereby blocking the calling system waiting for a result.
|
||||||
|
* **Stack**: A malignant script may consume attempt an infinite recursive call that exhausts the call stack.
|
||||||
|
* **Overflows**: A malignant script may deliberately cause numeric over-flows and/or under-flows, and/or bad
|
||||||
|
floating-point representations, in order to crash the system.
|
||||||
|
* **Files**: A malignant script may continuously `import` an external module within an infinite loop,
|
||||||
|
thereby putting heavy load on the file-system (or even the network if the file is not local).
|
||||||
|
* **Data**: A malignant script may attempt to read from and/or write to data that it does not own. If this happens,
|
||||||
|
it is a severe security breach and may put the entire system at risk.
|
||||||
|
|
||||||
|
### Maximum number of operations
|
||||||
|
|
||||||
|
Rhai by default does not limit how much time or CPU a script consumes.
|
||||||
|
This can be changed via the `Engine::set_max_operations` method, with zero being unlimited (the default).
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut engine = Engine::new();
|
||||||
|
|
||||||
|
engine.set_max_operations(500); // allow only up to 500 operations for this script
|
||||||
|
|
||||||
|
engine.set_max_operations(0); // allow unlimited operations
|
||||||
|
```
|
||||||
|
|
||||||
|
The concept of one single _operation_ in Rhai is volatile - it roughly equals one expression node,
|
||||||
|
one statement, one iteration of a loop, or one function call etc. with sub-expressions and statement
|
||||||
|
blocks executed inside these contexts accumulated on top.
|
||||||
|
|
||||||
|
One _operation_ can take an unspecified amount of time and CPU cycles, depending on the particular operation
|
||||||
|
involved. For example, loading a constant consumes very few CPU cycles, while calling an external Rust function,
|
||||||
|
though also counted as only one operation, may consume much more resources.
|
||||||
|
|
||||||
|
The _operation count_ is intended to be a very course-grained measurement of the amount of CPU that a script
|
||||||
|
is consuming, and allows the system to impose a hard upper limit.
|
||||||
|
|
||||||
|
A script exceeding the maximum operations count will terminate with an error result.
|
||||||
|
This check can be disabled via the [`unchecked`] feature for higher performance
|
||||||
|
(but higher risks as well).
|
||||||
|
|
||||||
|
### Tracking progress
|
||||||
|
|
||||||
|
To track script evaluation progress and to force-terminate a script prematurely (for any reason),
|
||||||
|
provide a closure to the `Engine::on_progress` method:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut engine = Engine::new();
|
||||||
|
|
||||||
|
engine.on_progress(|count| { // 'count' is the number of operations performed
|
||||||
|
if count % 1000 == 0 {
|
||||||
|
println!("{}", count); // print out a progress log every 1,000 operations
|
||||||
|
}
|
||||||
|
true // return 'true' to continue the script
|
||||||
|
// returning 'false' will terminate the script
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The closure passed to `Engine::on_progress` will be called once every operation.
|
||||||
|
Return `false` to terminate the script immediately.
|
||||||
|
|
||||||
|
### Maximum stack depth
|
||||||
|
|
||||||
|
Rhai by default limits function calls to a maximum depth of 256 levels (28 levels in debug build).
|
||||||
|
This limit may be changed via the `Engine::set_max_call_levels` method.
|
||||||
|
The limit can be disabled via the [`unchecked`] feature for higher performance
|
||||||
|
(but higher risks as well).
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut engine = Engine::new();
|
||||||
|
|
||||||
|
engine.set_max_call_levels(10); // allow only up to 10 levels of function calls
|
||||||
|
|
||||||
|
engine.set_max_call_levels(0); // allow no function calls at all (max depth = zero)
|
||||||
|
```
|
||||||
|
|
||||||
|
A script exceeding the maximum call stack depth will terminate with an error result.
|
||||||
|
|
||||||
|
### Checked arithmetic
|
||||||
|
|
||||||
|
All arithmetic calculations in Rhai are _checked_, meaning that the script terminates with an error whenever
|
||||||
|
it detects a numeric over-flow/under-flow condition or an invalid floating-point operation, instead of
|
||||||
|
crashing the entire system. This checking can be turned off via the [`unchecked`] feature for higher performance
|
||||||
|
(but higher risks as well).
|
||||||
|
|
||||||
|
### Access to external data
|
||||||
|
|
||||||
|
Rhai is _sand-boxed_ so a script can never read from outside its own environment.
|
||||||
|
Furthermore, an [`Engine`] created non-`mut` cannot mutate any state outside of itself;
|
||||||
|
so it is highly recommended that [`Engine`]'s are created immutable as much as possible.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut engine = Engine::new(); // create mutable 'Engine'
|
||||||
|
|
||||||
|
engine.register_get("add", add); // configure 'engine'
|
||||||
|
|
||||||
|
let engine = engine; // shadow the variable so that 'engine' is now immutable
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
Script optimization
|
Script optimization
|
||||||
===================
|
===================
|
||||||
|
|
||||||
@ -2358,7 +2468,7 @@ There are actually three levels of optimizations: `None`, `Simple` and `Full`.
|
|||||||
* `Full` is _much_ more aggressive, _including_ running functions on constant arguments to determine their result.
|
* `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.
|
One benefit to this is that many more optimization opportunities arise, especially with regards to comparison operators.
|
||||||
|
|
||||||
An [`Engine`]'s optimization level is set via a call to `set_optimization_level`:
|
An [`Engine`]'s optimization level is set via a call to `Engine::set_optimization_level`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Turn on aggressive optimizations
|
// Turn on aggressive optimizations
|
||||||
|
110
src/api.rs
110
src/api.rs
@ -865,7 +865,7 @@ impl Engine {
|
|||||||
scope: &mut Scope,
|
scope: &mut Scope,
|
||||||
ast: &AST,
|
ast: &AST,
|
||||||
) -> Result<T, Box<EvalAltResult>> {
|
) -> Result<T, Box<EvalAltResult>> {
|
||||||
let result = self.eval_ast_with_scope_raw(scope, ast)?;
|
let (result, _) = self.eval_ast_with_scope_raw(scope, ast)?;
|
||||||
|
|
||||||
let return_type = self.map_type_name(result.type_name());
|
let return_type = self.map_type_name(result.type_name());
|
||||||
|
|
||||||
@ -881,7 +881,7 @@ impl Engine {
|
|||||||
&self,
|
&self,
|
||||||
scope: &mut Scope,
|
scope: &mut Scope,
|
||||||
ast: &AST,
|
ast: &AST,
|
||||||
) -> Result<Dynamic, Box<EvalAltResult>> {
|
) -> Result<(Dynamic, u64), Box<EvalAltResult>> {
|
||||||
let mut state = State::new(ast.fn_lib());
|
let mut state = State::new(ast.fn_lib());
|
||||||
|
|
||||||
ast.statements()
|
ast.statements()
|
||||||
@ -893,6 +893,7 @@ impl Engine {
|
|||||||
EvalAltResult::Return(out, _) => Ok(out),
|
EvalAltResult::Return(out, _) => Ok(out),
|
||||||
_ => Err(err),
|
_ => Err(err),
|
||||||
})
|
})
|
||||||
|
.map(|v| (v, state.operations))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Evaluate a file, but throw away the result and only return error (if any).
|
/// Evaluate a file, but throw away the result and only return error (if any).
|
||||||
@ -1015,10 +1016,11 @@ impl Engine {
|
|||||||
.get_function_by_signature(name, args.len(), true)
|
.get_function_by_signature(name, args.len(), true)
|
||||||
.ok_or_else(|| Box::new(EvalAltResult::ErrorFunctionNotFound(name.into(), pos)))?;
|
.ok_or_else(|| Box::new(EvalAltResult::ErrorFunctionNotFound(name.into(), pos)))?;
|
||||||
|
|
||||||
let state = State::new(fn_lib);
|
let mut state = State::new(fn_lib);
|
||||||
|
let args = args.as_mut();
|
||||||
|
|
||||||
let result =
|
let (result, _) =
|
||||||
self.call_script_fn(Some(scope), &state, name, fn_def, args.as_mut(), pos, 0)?;
|
self.call_script_fn(Some(scope), &mut state, name, fn_def, args, pos, 0, 0)?;
|
||||||
|
|
||||||
let return_type = self.map_type_name(result.type_name());
|
let return_type = self.map_type_name(result.type_name());
|
||||||
|
|
||||||
@ -1058,6 +1060,84 @@ impl Engine {
|
|||||||
optimize_into_ast(self, scope, stmt, fn_lib, optimization_level)
|
optimize_into_ast(self, scope, stmt, fn_lib, optimization_level)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a callback for script evaluation progress.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
|
||||||
|
/// # use std::sync::RwLock;
|
||||||
|
/// # use std::sync::Arc;
|
||||||
|
/// use rhai::Engine;
|
||||||
|
///
|
||||||
|
/// let result = Arc::new(RwLock::new(0_u64));
|
||||||
|
/// let logger = result.clone();
|
||||||
|
///
|
||||||
|
/// let mut engine = Engine::new();
|
||||||
|
///
|
||||||
|
/// engine.on_progress(move |ops| {
|
||||||
|
/// if ops > 10000 {
|
||||||
|
/// false
|
||||||
|
/// } else if ops % 800 == 0 {
|
||||||
|
/// *logger.write().unwrap() = ops;
|
||||||
|
/// true
|
||||||
|
/// } else {
|
||||||
|
/// true
|
||||||
|
/// }
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// engine.consume("for x in range(0, 50000) {}")
|
||||||
|
/// .expect_err("should error");
|
||||||
|
///
|
||||||
|
/// assert_eq!(*result.read().unwrap(), 9600);
|
||||||
|
///
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
#[cfg(feature = "sync")]
|
||||||
|
pub fn on_progress(&mut self, callback: impl Fn(u64) -> bool + Send + Sync + 'static) {
|
||||||
|
self.progress = Some(Box::new(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a callback for script evaluation progress.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
|
||||||
|
/// # use std::cell::Cell;
|
||||||
|
/// # use std::rc::Rc;
|
||||||
|
/// use rhai::Engine;
|
||||||
|
///
|
||||||
|
/// let result = Rc::new(Cell::new(0_u64));
|
||||||
|
/// let logger = result.clone();
|
||||||
|
///
|
||||||
|
/// let mut engine = Engine::new();
|
||||||
|
///
|
||||||
|
/// engine.on_progress(move |ops| {
|
||||||
|
/// if ops > 10000 {
|
||||||
|
/// false
|
||||||
|
/// } else if ops % 800 == 0 {
|
||||||
|
/// logger.set(ops);
|
||||||
|
/// true
|
||||||
|
/// } else {
|
||||||
|
/// true
|
||||||
|
/// }
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// engine.consume("for x in range(0, 50000) {}")
|
||||||
|
/// .expect_err("should error");
|
||||||
|
///
|
||||||
|
/// assert_eq!(result.get(), 9600);
|
||||||
|
///
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
#[cfg(not(feature = "sync"))]
|
||||||
|
pub fn on_progress(&mut self, callback: impl Fn(u64) -> bool + 'static) {
|
||||||
|
self.progress = Some(Box::new(callback));
|
||||||
|
}
|
||||||
|
|
||||||
/// Override default action of `print` (print to stdout using `println!`)
|
/// Override default action of `print` (print to stdout using `println!`)
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
@ -1092,21 +1172,21 @@ impl Engine {
|
|||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
|
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
|
||||||
/// # use std::sync::RwLock;
|
/// # use std::cell::RefCell;
|
||||||
/// # use std::sync::Arc;
|
/// # use std::rc::Rc;
|
||||||
/// use rhai::Engine;
|
/// use rhai::Engine;
|
||||||
///
|
///
|
||||||
/// let result = Arc::new(RwLock::new(String::from("")));
|
/// let result = Rc::new(RefCell::new(String::from("")));
|
||||||
///
|
///
|
||||||
/// let mut engine = Engine::new();
|
/// let mut engine = Engine::new();
|
||||||
///
|
///
|
||||||
/// // Override action of 'print' function
|
/// // Override action of 'print' function
|
||||||
/// let logger = result.clone();
|
/// let logger = result.clone();
|
||||||
/// engine.on_print(move |s| logger.write().unwrap().push_str(s));
|
/// engine.on_print(move |s| logger.borrow_mut().push_str(s));
|
||||||
///
|
///
|
||||||
/// engine.consume("print(40 + 2);")?;
|
/// engine.consume("print(40 + 2);")?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(*result.read().unwrap(), "42");
|
/// assert_eq!(*result.borrow(), "42");
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
@ -1149,21 +1229,21 @@ impl Engine {
|
|||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
|
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
|
||||||
/// # use std::sync::RwLock;
|
/// # use std::cell::RefCell;
|
||||||
/// # use std::sync::Arc;
|
/// # use std::rc::Rc;
|
||||||
/// use rhai::Engine;
|
/// use rhai::Engine;
|
||||||
///
|
///
|
||||||
/// let result = Arc::new(RwLock::new(String::from("")));
|
/// let result = Rc::new(RefCell::new(String::from("")));
|
||||||
///
|
///
|
||||||
/// let mut engine = Engine::new();
|
/// let mut engine = Engine::new();
|
||||||
///
|
///
|
||||||
/// // Override action of 'print' function
|
/// // Override action of 'print' function
|
||||||
/// let logger = result.clone();
|
/// let logger = result.clone();
|
||||||
/// engine.on_debug(move |s| logger.write().unwrap().push_str(s));
|
/// engine.on_debug(move |s| logger.borrow_mut().push_str(s));
|
||||||
///
|
///
|
||||||
/// engine.consume(r#"debug("hello");"#)?;
|
/// engine.consume(r#"debug("hello");"#)?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(*result.read().unwrap(), r#""hello""#);
|
/// assert_eq!(*result.borrow(), r#""hello""#);
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
|
204
src/engine.rs
204
src/engine.rs
@ -3,7 +3,7 @@
|
|||||||
use crate::any::{Dynamic, Union};
|
use crate::any::{Dynamic, Union};
|
||||||
use crate::calc_fn_hash;
|
use crate::calc_fn_hash;
|
||||||
use crate::error::ParseErrorType;
|
use crate::error::ParseErrorType;
|
||||||
use crate::fn_native::{FnCallArgs, NativeFunctionABI, PrintCallback};
|
use crate::fn_native::{FnCallArgs, NativeFunctionABI, PrintCallback, ProgressCallback};
|
||||||
use crate::module::Module;
|
use crate::module::Module;
|
||||||
use crate::optimize::OptimizationLevel;
|
use crate::optimize::OptimizationLevel;
|
||||||
use crate::packages::{CorePackage, Package, PackageLibrary, PackagesCollection, StandardPackage};
|
use crate::packages::{CorePackage, Package, PackageLibrary, PackagesCollection, StandardPackage};
|
||||||
@ -28,7 +28,7 @@ use crate::stdlib::{
|
|||||||
format,
|
format,
|
||||||
iter::{empty, once, repeat},
|
iter::{empty, once, repeat},
|
||||||
mem,
|
mem,
|
||||||
num::NonZeroUsize,
|
num::{NonZeroU64, NonZeroUsize},
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
string::{String, ToString},
|
string::{String, ToString},
|
||||||
@ -48,12 +48,17 @@ pub type Array = Vec<Dynamic>;
|
|||||||
#[cfg(not(feature = "no_object"))]
|
#[cfg(not(feature = "no_object"))]
|
||||||
pub type Map = HashMap<String, Dynamic>;
|
pub type Map = HashMap<String, Dynamic>;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "unchecked"))]
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
pub const MAX_CALL_STACK_DEPTH: usize = 28;
|
pub const MAX_CALL_STACK_DEPTH: usize = 28;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "unchecked"))]
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
pub const MAX_CALL_STACK_DEPTH: usize = 256;
|
pub const MAX_CALL_STACK_DEPTH: usize = 256;
|
||||||
|
|
||||||
|
#[cfg(feature = "unchecked")]
|
||||||
|
pub const MAX_CALL_STACK_DEPTH: usize = usize::MAX;
|
||||||
|
|
||||||
pub const KEYWORD_PRINT: &str = "print";
|
pub const KEYWORD_PRINT: &str = "print";
|
||||||
pub const KEYWORD_DEBUG: &str = "debug";
|
pub const KEYWORD_DEBUG: &str = "debug";
|
||||||
pub const KEYWORD_TYPE_OF: &str = "type_of";
|
pub const KEYWORD_TYPE_OF: &str = "type_of";
|
||||||
@ -146,6 +151,9 @@ pub struct State<'a> {
|
|||||||
/// Level of the current scope. The global (root) level is zero, a new block (or function call)
|
/// Level of the current scope. The global (root) level is zero, a new block (or function call)
|
||||||
/// is one level higher, and so on.
|
/// is one level higher, and so on.
|
||||||
pub scope_level: usize,
|
pub scope_level: usize,
|
||||||
|
|
||||||
|
/// Number of operations performed.
|
||||||
|
pub operations: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> State<'a> {
|
impl<'a> State<'a> {
|
||||||
@ -155,6 +163,7 @@ impl<'a> State<'a> {
|
|||||||
always_search: false,
|
always_search: false,
|
||||||
fn_lib,
|
fn_lib,
|
||||||
scope_level: 0,
|
scope_level: 0,
|
||||||
|
operations: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Does a certain script-defined function exist in the `State`?
|
/// Does a certain script-defined function exist in the `State`?
|
||||||
@ -304,6 +313,8 @@ pub struct Engine {
|
|||||||
pub(crate) print: Box<PrintCallback>,
|
pub(crate) print: Box<PrintCallback>,
|
||||||
/// Closure for implementing the `debug` command.
|
/// Closure for implementing the `debug` command.
|
||||||
pub(crate) debug: Box<PrintCallback>,
|
pub(crate) debug: Box<PrintCallback>,
|
||||||
|
/// Closure for progress reporting.
|
||||||
|
pub(crate) progress: Option<Box<ProgressCallback>>,
|
||||||
|
|
||||||
/// Optimize the AST after compilation.
|
/// Optimize the AST after compilation.
|
||||||
pub(crate) optimization_level: OptimizationLevel,
|
pub(crate) optimization_level: OptimizationLevel,
|
||||||
@ -311,6 +322,8 @@ pub struct Engine {
|
|||||||
///
|
///
|
||||||
/// Defaults to 28 for debug builds and 256 for non-debug builds.
|
/// Defaults to 28 for debug builds and 256 for non-debug builds.
|
||||||
pub(crate) max_call_stack_depth: usize,
|
pub(crate) max_call_stack_depth: usize,
|
||||||
|
/// Maximum number of operations to run.
|
||||||
|
pub(crate) max_operations: Option<NonZeroU64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Engine {
|
impl Default for Engine {
|
||||||
@ -333,6 +346,9 @@ impl Default for Engine {
|
|||||||
print: Box::new(default_print),
|
print: Box::new(default_print),
|
||||||
debug: Box::new(default_print),
|
debug: Box::new(default_print),
|
||||||
|
|
||||||
|
// progress callback
|
||||||
|
progress: None,
|
||||||
|
|
||||||
// optimization level
|
// optimization level
|
||||||
#[cfg(feature = "no_optimize")]
|
#[cfg(feature = "no_optimize")]
|
||||||
optimization_level: OptimizationLevel::None,
|
optimization_level: OptimizationLevel::None,
|
||||||
@ -346,6 +362,7 @@ impl Default for Engine {
|
|||||||
optimization_level: OptimizationLevel::Full,
|
optimization_level: OptimizationLevel::Full,
|
||||||
|
|
||||||
max_call_stack_depth: MAX_CALL_STACK_DEPTH,
|
max_call_stack_depth: MAX_CALL_STACK_DEPTH,
|
||||||
|
max_operations: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "no_stdlib")]
|
#[cfg(feature = "no_stdlib")]
|
||||||
@ -471,6 +488,7 @@ impl Engine {
|
|||||||
type_names: Default::default(),
|
type_names: Default::default(),
|
||||||
print: Box::new(|_| {}),
|
print: Box::new(|_| {}),
|
||||||
debug: Box::new(|_| {}),
|
debug: Box::new(|_| {}),
|
||||||
|
progress: None,
|
||||||
|
|
||||||
#[cfg(feature = "no_optimize")]
|
#[cfg(feature = "no_optimize")]
|
||||||
optimization_level: OptimizationLevel::None,
|
optimization_level: OptimizationLevel::None,
|
||||||
@ -484,6 +502,7 @@ impl Engine {
|
|||||||
optimization_level: OptimizationLevel::Full,
|
optimization_level: OptimizationLevel::Full,
|
||||||
|
|
||||||
max_call_stack_depth: MAX_CALL_STACK_DEPTH,
|
max_call_stack_depth: MAX_CALL_STACK_DEPTH,
|
||||||
|
max_operations: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -515,10 +534,17 @@ impl Engine {
|
|||||||
|
|
||||||
/// Set the maximum levels of function calls allowed for a script in order to avoid
|
/// Set the maximum levels of function calls allowed for a script in order to avoid
|
||||||
/// infinite recursion and stack overflows.
|
/// infinite recursion and stack overflows.
|
||||||
|
#[cfg(not(feature = "unchecked"))]
|
||||||
pub fn set_max_call_levels(&mut self, levels: usize) {
|
pub fn set_max_call_levels(&mut self, levels: usize) {
|
||||||
self.max_call_stack_depth = levels
|
self.max_call_stack_depth = levels
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the maximum number of operations allowed for a script to run to avoid
|
||||||
|
/// consuming too much resources (0 for unlimited).
|
||||||
|
#[cfg(not(feature = "unchecked"))]
|
||||||
|
pub fn set_max_operations(&mut self, operations: u64) {
|
||||||
|
self.max_operations = NonZeroU64::new(operations);
|
||||||
|
}
|
||||||
/// Set the module resolution service used by the `Engine`.
|
/// Set the module resolution service used by the `Engine`.
|
||||||
///
|
///
|
||||||
/// Not available under the `no_module` feature.
|
/// Not available under the `no_module` feature.
|
||||||
@ -537,7 +563,7 @@ impl Engine {
|
|||||||
pub(crate) fn call_fn_raw(
|
pub(crate) fn call_fn_raw(
|
||||||
&self,
|
&self,
|
||||||
scope: Option<&mut Scope>,
|
scope: Option<&mut Scope>,
|
||||||
state: &State,
|
state: &mut State,
|
||||||
fn_name: &str,
|
fn_name: &str,
|
||||||
hashes: (u64, u64),
|
hashes: (u64, u64),
|
||||||
args: &mut FnCallArgs,
|
args: &mut FnCallArgs,
|
||||||
@ -546,6 +572,8 @@ impl Engine {
|
|||||||
pos: Position,
|
pos: Position,
|
||||||
level: usize,
|
level: usize,
|
||||||
) -> Result<(Dynamic, bool), Box<EvalAltResult>> {
|
) -> Result<(Dynamic, bool), Box<EvalAltResult>> {
|
||||||
|
self.inc_operations(state, pos)?;
|
||||||
|
|
||||||
// Check for stack overflow
|
// Check for stack overflow
|
||||||
if level > self.max_call_stack_depth {
|
if level > self.max_call_stack_depth {
|
||||||
return Err(Box::new(EvalAltResult::ErrorStackOverflow(pos)));
|
return Err(Box::new(EvalAltResult::ErrorStackOverflow(pos)));
|
||||||
@ -554,9 +582,12 @@ impl Engine {
|
|||||||
// First search in script-defined functions (can override built-in)
|
// First search in script-defined functions (can override built-in)
|
||||||
if hashes.1 > 0 {
|
if hashes.1 > 0 {
|
||||||
if let Some(fn_def) = state.get_function(hashes.1) {
|
if let Some(fn_def) = state.get_function(hashes.1) {
|
||||||
return self
|
let ops = state.operations;
|
||||||
.call_script_fn(scope, state, fn_name, fn_def, args, pos, level)
|
let (result, operations) =
|
||||||
.map(|v| (v, false));
|
self.call_script_fn(scope, &state, fn_name, fn_def, args, pos, level, ops)?;
|
||||||
|
state.operations = operations;
|
||||||
|
self.inc_operations(state, pos)?;
|
||||||
|
return Ok((result, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -677,14 +708,17 @@ impl Engine {
|
|||||||
args: &mut FnCallArgs,
|
args: &mut FnCallArgs,
|
||||||
pos: Position,
|
pos: Position,
|
||||||
level: usize,
|
level: usize,
|
||||||
) -> Result<Dynamic, Box<EvalAltResult>> {
|
operations: u64,
|
||||||
|
) -> Result<(Dynamic, u64), Box<EvalAltResult>> {
|
||||||
match scope {
|
match scope {
|
||||||
// Extern scope passed in which is not empty
|
// Extern scope passed in which is not empty
|
||||||
Some(scope) if scope.len() > 0 => {
|
Some(scope) if scope.len() > 0 => {
|
||||||
let scope_len = scope.len();
|
let scope_len = scope.len();
|
||||||
let mut state = State::new(state.fn_lib);
|
let mut local_state = State::new(state.fn_lib);
|
||||||
|
|
||||||
state.scope_level += 1;
|
local_state.operations = operations;
|
||||||
|
self.inc_operations(&mut local_state, pos)?;
|
||||||
|
local_state.scope_level += 1;
|
||||||
|
|
||||||
// Put arguments into scope as variables
|
// Put arguments into scope as variables
|
||||||
scope.extend(
|
scope.extend(
|
||||||
@ -696,14 +730,14 @@ impl Engine {
|
|||||||
args.into_iter().map(|v| mem::take(*v)),
|
args.into_iter().map(|v| mem::take(*v)),
|
||||||
)
|
)
|
||||||
.map(|(name, value)| {
|
.map(|(name, value)| {
|
||||||
let var_name = unsafe_cast_var_name(name.as_str(), &state);
|
let var_name = unsafe_cast_var_name(name.as_str(), &local_state);
|
||||||
(var_name, ScopeEntryType::Normal, value)
|
(var_name, ScopeEntryType::Normal, value)
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Evaluate the function at one higher level of call depth
|
// Evaluate the function at one higher level of call depth
|
||||||
let result = self
|
let result = self
|
||||||
.eval_stmt(scope, &mut state, &fn_def.body, level + 1)
|
.eval_stmt(scope, &mut local_state, &fn_def.body, level + 1)
|
||||||
.or_else(|err| match *err {
|
.or_else(|err| match *err {
|
||||||
// Convert return statement to return value
|
// Convert return statement to return value
|
||||||
EvalAltResult::Return(x, _) => Ok(x),
|
EvalAltResult::Return(x, _) => Ok(x),
|
||||||
@ -725,13 +759,16 @@ impl Engine {
|
|||||||
// No need to reset `state.scope_level` because it is thrown away
|
// No need to reset `state.scope_level` because it is thrown away
|
||||||
scope.rewind(scope_len);
|
scope.rewind(scope_len);
|
||||||
|
|
||||||
return result;
|
return result.map(|v| (v, local_state.operations));
|
||||||
}
|
}
|
||||||
// No new scope - create internal scope
|
// No new scope - create internal scope
|
||||||
_ => {
|
_ => {
|
||||||
let mut scope = Scope::new();
|
let mut scope = Scope::new();
|
||||||
let mut state = State::new(state.fn_lib);
|
let mut local_state = State::new(state.fn_lib);
|
||||||
state.scope_level += 1;
|
|
||||||
|
local_state.operations = operations;
|
||||||
|
self.inc_operations(&mut local_state, pos)?;
|
||||||
|
local_state.scope_level += 1;
|
||||||
|
|
||||||
// Put arguments into scope as variables
|
// Put arguments into scope as variables
|
||||||
scope.extend(
|
scope.extend(
|
||||||
@ -748,7 +785,7 @@ impl Engine {
|
|||||||
// Evaluate the function at one higher level of call depth
|
// Evaluate the function at one higher level of call depth
|
||||||
// No need to reset `state.scope_level` because it is thrown away
|
// No need to reset `state.scope_level` because it is thrown away
|
||||||
return self
|
return self
|
||||||
.eval_stmt(&mut scope, &mut state, &fn_def.body, level + 1)
|
.eval_stmt(&mut scope, &mut local_state, &fn_def.body, level + 1)
|
||||||
.or_else(|err| match *err {
|
.or_else(|err| match *err {
|
||||||
// Convert return statement to return value
|
// Convert return statement to return value
|
||||||
EvalAltResult::Return(x, _) => Ok(x),
|
EvalAltResult::Return(x, _) => Ok(x),
|
||||||
@ -764,7 +801,8 @@ impl Engine {
|
|||||||
err,
|
err,
|
||||||
pos,
|
pos,
|
||||||
))),
|
))),
|
||||||
});
|
})
|
||||||
|
.map(|v| (v, local_state.operations));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -788,7 +826,7 @@ impl Engine {
|
|||||||
/// **DO NOT** reuse the argument values unless for the first `&mut` argument - all others are silently replaced by `()`!
|
/// **DO NOT** reuse the argument values unless for the first `&mut` argument - all others are silently replaced by `()`!
|
||||||
fn exec_fn_call(
|
fn exec_fn_call(
|
||||||
&self,
|
&self,
|
||||||
state: &State,
|
state: &mut State,
|
||||||
fn_name: &str,
|
fn_name: &str,
|
||||||
hash_fn_def: u64,
|
hash_fn_def: u64,
|
||||||
args: &mut FnCallArgs,
|
args: &mut FnCallArgs,
|
||||||
@ -827,7 +865,7 @@ impl Engine {
|
|||||||
fn eval_script_expr(
|
fn eval_script_expr(
|
||||||
&self,
|
&self,
|
||||||
scope: &mut Scope,
|
scope: &mut Scope,
|
||||||
state: &State,
|
state: &mut State,
|
||||||
script: &Dynamic,
|
script: &Dynamic,
|
||||||
pos: Position,
|
pos: Position,
|
||||||
) -> Result<Dynamic, Box<EvalAltResult>> {
|
) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||||
@ -854,14 +892,20 @@ impl Engine {
|
|||||||
let ast = AST::new(statements, state.fn_lib.clone());
|
let ast = AST::new(statements, state.fn_lib.clone());
|
||||||
|
|
||||||
// Evaluate the AST
|
// Evaluate the AST
|
||||||
self.eval_ast_with_scope_raw(scope, &ast)
|
let (result, operations) = self
|
||||||
.map_err(|err| err.new_position(pos))
|
.eval_ast_with_scope_raw(scope, &ast)
|
||||||
|
.map_err(|err| err.new_position(pos))?;
|
||||||
|
|
||||||
|
state.operations += operations;
|
||||||
|
self.inc_operations(state, pos)?;
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Chain-evaluate a dot/index chain.
|
/// Chain-evaluate a dot/index chain.
|
||||||
fn eval_dot_index_chain_helper(
|
fn eval_dot_index_chain_helper(
|
||||||
&self,
|
&self,
|
||||||
state: &State,
|
state: &mut State,
|
||||||
mut target: Target,
|
mut target: Target,
|
||||||
rhs: &Expr,
|
rhs: &Expr,
|
||||||
idx_values: &mut StaticVec<Dynamic>,
|
idx_values: &mut StaticVec<Dynamic>,
|
||||||
@ -1145,7 +1189,7 @@ impl Engine {
|
|||||||
/// Get the value at the indexed position of a base type
|
/// Get the value at the indexed position of a base type
|
||||||
fn get_indexed_mut<'a>(
|
fn get_indexed_mut<'a>(
|
||||||
&self,
|
&self,
|
||||||
state: &State,
|
state: &mut State,
|
||||||
val: &'a mut Dynamic,
|
val: &'a mut Dynamic,
|
||||||
is_ref: bool,
|
is_ref: bool,
|
||||||
mut idx: Dynamic,
|
mut idx: Dynamic,
|
||||||
@ -1153,6 +1197,8 @@ impl Engine {
|
|||||||
op_pos: Position,
|
op_pos: Position,
|
||||||
create: bool,
|
create: bool,
|
||||||
) -> Result<Target<'a>, Box<EvalAltResult>> {
|
) -> Result<Target<'a>, Box<EvalAltResult>> {
|
||||||
|
self.inc_operations(state, op_pos)?;
|
||||||
|
|
||||||
match val {
|
match val {
|
||||||
#[cfg(not(feature = "no_index"))]
|
#[cfg(not(feature = "no_index"))]
|
||||||
Dynamic(Union::Array(arr)) => {
|
Dynamic(Union::Array(arr)) => {
|
||||||
@ -1234,6 +1280,8 @@ impl Engine {
|
|||||||
rhs: &Expr,
|
rhs: &Expr,
|
||||||
level: usize,
|
level: usize,
|
||||||
) -> Result<Dynamic, Box<EvalAltResult>> {
|
) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||||
|
self.inc_operations(state, rhs.position())?;
|
||||||
|
|
||||||
let mut lhs_value = self.eval_expr(scope, state, lhs, level)?;
|
let mut lhs_value = self.eval_expr(scope, state, lhs, level)?;
|
||||||
let rhs_value = self.eval_expr(scope, state, rhs, level)?;
|
let rhs_value = self.eval_expr(scope, state, rhs, level)?;
|
||||||
|
|
||||||
@ -1288,6 +1336,8 @@ impl Engine {
|
|||||||
expr: &Expr,
|
expr: &Expr,
|
||||||
level: usize,
|
level: usize,
|
||||||
) -> Result<Dynamic, Box<EvalAltResult>> {
|
) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||||
|
self.inc_operations(state, expr.position())?;
|
||||||
|
|
||||||
match expr {
|
match expr {
|
||||||
Expr::IntegerConstant(x) => Ok(x.0.into()),
|
Expr::IntegerConstant(x) => Ok(x.0.into()),
|
||||||
#[cfg(not(feature = "no_float"))]
|
#[cfg(not(feature = "no_float"))]
|
||||||
@ -1461,9 +1511,16 @@ impl Engine {
|
|||||||
|
|
||||||
// First search in script-defined functions (can override built-in)
|
// First search in script-defined functions (can override built-in)
|
||||||
if let Some(fn_def) = module.get_qualified_scripted_fn(*hash_fn_def) {
|
if let Some(fn_def) = module.get_qualified_scripted_fn(*hash_fn_def) {
|
||||||
self.call_script_fn(None, state, name, fn_def, args.as_mut(), *pos, level)
|
let args = args.as_mut();
|
||||||
|
let ops = state.operations;
|
||||||
|
let (result, operations) =
|
||||||
|
self.call_script_fn(None, state, name, fn_def, args, *pos, level, ops)?;
|
||||||
|
state.operations = operations;
|
||||||
|
self.inc_operations(state, *pos)?;
|
||||||
|
Ok(result)
|
||||||
} else {
|
} else {
|
||||||
// Then search in Rust functions
|
// Then search in Rust functions
|
||||||
|
self.inc_operations(state, *pos)?;
|
||||||
|
|
||||||
// Rust functions are indexed in two steps:
|
// Rust functions are indexed in two steps:
|
||||||
// 1) Calculate a hash in a similar manner to script-defined functions,
|
// 1) Calculate a hash in a similar manner to script-defined functions,
|
||||||
@ -1538,6 +1595,8 @@ impl Engine {
|
|||||||
stmt: &Stmt,
|
stmt: &Stmt,
|
||||||
level: usize,
|
level: usize,
|
||||||
) -> Result<Dynamic, Box<EvalAltResult>> {
|
) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||||
|
self.inc_operations(state, stmt.position())?;
|
||||||
|
|
||||||
match stmt {
|
match stmt {
|
||||||
// No-op
|
// No-op
|
||||||
Stmt::Noop(_) => Ok(Default::default()),
|
Stmt::Noop(_) => Ok(Default::default()),
|
||||||
@ -1546,11 +1605,10 @@ impl Engine {
|
|||||||
Stmt::Expr(expr) => {
|
Stmt::Expr(expr) => {
|
||||||
let result = self.eval_expr(scope, state, expr, level)?;
|
let result = self.eval_expr(scope, state, expr, level)?;
|
||||||
|
|
||||||
Ok(if let Expr::Assignment(_) = *expr.as_ref() {
|
Ok(match expr.as_ref() {
|
||||||
// If it is an assignment, erase the result at the root
|
// If it is an assignment, erase the result at the root
|
||||||
Default::default()
|
Expr::Assignment(_) => Default::default(),
|
||||||
} else {
|
_ => result,
|
||||||
result
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1574,25 +1632,32 @@ impl Engine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If-else statement
|
// If-else statement
|
||||||
Stmt::IfThenElse(x) => self
|
Stmt::IfThenElse(x) => {
|
||||||
.eval_expr(scope, state, &x.0, level)?
|
let (expr, if_block, else_block) = x.as_ref();
|
||||||
.as_bool()
|
|
||||||
.map_err(|_| Box::new(EvalAltResult::ErrorLogicGuard(x.0.position())))
|
self.eval_expr(scope, state, expr, level)?
|
||||||
.and_then(|guard_val| {
|
.as_bool()
|
||||||
if guard_val {
|
.map_err(|_| Box::new(EvalAltResult::ErrorLogicGuard(expr.position())))
|
||||||
self.eval_stmt(scope, state, &x.1, level)
|
.and_then(|guard_val| {
|
||||||
} else if let Some(stmt) = &x.2 {
|
if guard_val {
|
||||||
self.eval_stmt(scope, state, stmt, level)
|
self.eval_stmt(scope, state, if_block, level)
|
||||||
} else {
|
} else if let Some(stmt) = else_block {
|
||||||
Ok(Default::default())
|
self.eval_stmt(scope, state, stmt, level)
|
||||||
}
|
} else {
|
||||||
}),
|
Ok(Default::default())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// While loop
|
// While loop
|
||||||
Stmt::While(x) => loop {
|
Stmt::While(x) => loop {
|
||||||
match self.eval_expr(scope, state, &x.0, level)?.as_bool() {
|
let (expr, body) = x.as_ref();
|
||||||
Ok(true) => match self.eval_stmt(scope, state, &x.1, level) {
|
|
||||||
Ok(_) => (),
|
match self.eval_expr(scope, state, expr, level)?.as_bool() {
|
||||||
|
Ok(true) => match self.eval_stmt(scope, state, body, level) {
|
||||||
|
Ok(_) => {
|
||||||
|
self.inc_operations(state, body.position())?;
|
||||||
|
}
|
||||||
Err(err) => match *err {
|
Err(err) => match *err {
|
||||||
EvalAltResult::ErrorLoopBreak(false, _) => (),
|
EvalAltResult::ErrorLoopBreak(false, _) => (),
|
||||||
EvalAltResult::ErrorLoopBreak(true, _) => return Ok(Default::default()),
|
EvalAltResult::ErrorLoopBreak(true, _) => return Ok(Default::default()),
|
||||||
@ -1600,14 +1665,18 @@ impl Engine {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Ok(false) => return Ok(Default::default()),
|
Ok(false) => return Ok(Default::default()),
|
||||||
Err(_) => return Err(Box::new(EvalAltResult::ErrorLogicGuard(x.0.position()))),
|
Err(_) => {
|
||||||
|
return Err(Box::new(EvalAltResult::ErrorLogicGuard(expr.position())))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Loop statement
|
// Loop statement
|
||||||
Stmt::Loop(body) => loop {
|
Stmt::Loop(body) => loop {
|
||||||
match self.eval_stmt(scope, state, body, level) {
|
match self.eval_stmt(scope, state, body, level) {
|
||||||
Ok(_) => (),
|
Ok(_) => {
|
||||||
|
self.inc_operations(state, body.position())?;
|
||||||
|
}
|
||||||
Err(err) => match *err {
|
Err(err) => match *err {
|
||||||
EvalAltResult::ErrorLoopBreak(false, _) => (),
|
EvalAltResult::ErrorLoopBreak(false, _) => (),
|
||||||
EvalAltResult::ErrorLoopBreak(true, _) => return Ok(Default::default()),
|
EvalAltResult::ErrorLoopBreak(true, _) => return Ok(Default::default()),
|
||||||
@ -1618,7 +1687,8 @@ impl Engine {
|
|||||||
|
|
||||||
// For loop
|
// For loop
|
||||||
Stmt::For(x) => {
|
Stmt::For(x) => {
|
||||||
let iter_type = self.eval_expr(scope, state, &x.1, level)?;
|
let (name, expr, stmt) = x.as_ref();
|
||||||
|
let iter_type = self.eval_expr(scope, state, expr, level)?;
|
||||||
let tid = iter_type.type_id();
|
let tid = iter_type.type_id();
|
||||||
|
|
||||||
if let Some(iter_fn) = self
|
if let Some(iter_fn) = self
|
||||||
@ -1627,15 +1697,16 @@ impl Engine {
|
|||||||
.or_else(|| self.packages.get_iter(tid))
|
.or_else(|| self.packages.get_iter(tid))
|
||||||
{
|
{
|
||||||
// Add the loop variable
|
// Add the loop variable
|
||||||
let var_name = unsafe_cast_var_name(&x.0, &state);
|
let var_name = unsafe_cast_var_name(name, &state);
|
||||||
scope.push(var_name, ());
|
scope.push(var_name, ());
|
||||||
let index = scope.len() - 1;
|
let index = scope.len() - 1;
|
||||||
state.scope_level += 1;
|
state.scope_level += 1;
|
||||||
|
|
||||||
for loop_var in iter_fn(iter_type) {
|
for loop_var in iter_fn(iter_type) {
|
||||||
*scope.get_mut(index).0 = loop_var;
|
*scope.get_mut(index).0 = loop_var;
|
||||||
|
self.inc_operations(state, stmt.position())?;
|
||||||
|
|
||||||
match self.eval_stmt(scope, state, &x.2, level) {
|
match self.eval_stmt(scope, state, stmt, level) {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(err) => match *err {
|
Err(err) => match *err {
|
||||||
EvalAltResult::ErrorLoopBreak(false, _) => (),
|
EvalAltResult::ErrorLoopBreak(false, _) => (),
|
||||||
@ -1690,26 +1761,29 @@ impl Engine {
|
|||||||
|
|
||||||
// Let statement
|
// Let statement
|
||||||
Stmt::Let(x) if x.1.is_some() => {
|
Stmt::Let(x) if x.1.is_some() => {
|
||||||
let ((var_name, _), expr) = x.as_ref();
|
let ((var_name, pos), expr) = x.as_ref();
|
||||||
let val = self.eval_expr(scope, state, expr.as_ref().unwrap(), level)?;
|
let val = self.eval_expr(scope, state, expr.as_ref().unwrap(), level)?;
|
||||||
let var_name = unsafe_cast_var_name(var_name, &state);
|
let var_name = unsafe_cast_var_name(var_name, &state);
|
||||||
scope.push_dynamic_value(var_name, ScopeEntryType::Normal, val, false);
|
scope.push_dynamic_value(var_name, ScopeEntryType::Normal, val, false);
|
||||||
|
self.inc_operations(state, *pos)?;
|
||||||
Ok(Default::default())
|
Ok(Default::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
Stmt::Let(x) => {
|
Stmt::Let(x) => {
|
||||||
let ((var_name, _), _) = x.as_ref();
|
let ((var_name, pos), _) = x.as_ref();
|
||||||
let var_name = unsafe_cast_var_name(var_name, &state);
|
let var_name = unsafe_cast_var_name(var_name, &state);
|
||||||
scope.push(var_name, ());
|
scope.push(var_name, ());
|
||||||
|
self.inc_operations(state, *pos)?;
|
||||||
Ok(Default::default())
|
Ok(Default::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Const statement
|
// Const statement
|
||||||
Stmt::Const(x) if x.1.is_constant() => {
|
Stmt::Const(x) if x.1.is_constant() => {
|
||||||
let ((var_name, _), expr) = x.as_ref();
|
let ((var_name, pos), expr) = x.as_ref();
|
||||||
let val = self.eval_expr(scope, state, &expr, level)?;
|
let val = self.eval_expr(scope, state, &expr, level)?;
|
||||||
let var_name = unsafe_cast_var_name(var_name, &state);
|
let var_name = unsafe_cast_var_name(var_name, &state);
|
||||||
scope.push_dynamic_value(var_name, ScopeEntryType::Constant, val, true);
|
scope.push_dynamic_value(var_name, ScopeEntryType::Constant, val, true);
|
||||||
|
self.inc_operations(state, *pos)?;
|
||||||
Ok(Default::default())
|
Ok(Default::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1718,7 +1792,7 @@ impl Engine {
|
|||||||
|
|
||||||
// Import statement
|
// Import statement
|
||||||
Stmt::Import(x) => {
|
Stmt::Import(x) => {
|
||||||
let (expr, (name, _)) = x.as_ref();
|
let (expr, (name, pos)) = x.as_ref();
|
||||||
|
|
||||||
#[cfg(feature = "no_module")]
|
#[cfg(feature = "no_module")]
|
||||||
unreachable!();
|
unreachable!();
|
||||||
@ -1736,6 +1810,7 @@ impl Engine {
|
|||||||
|
|
||||||
let mod_name = unsafe_cast_var_name(name, &state);
|
let mod_name = unsafe_cast_var_name(name, &state);
|
||||||
scope.push_module(mod_name, module);
|
scope.push_module(mod_name, module);
|
||||||
|
self.inc_operations(state, *pos)?;
|
||||||
Ok(Default::default())
|
Ok(Default::default())
|
||||||
} else {
|
} else {
|
||||||
Err(Box::new(EvalAltResult::ErrorModuleNotFound(
|
Err(Box::new(EvalAltResult::ErrorModuleNotFound(
|
||||||
@ -1776,6 +1851,31 @@ impl Engine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if the number of operations stay within limit.
|
||||||
|
fn inc_operations(&self, state: &mut State, pos: Position) -> Result<(), Box<EvalAltResult>> {
|
||||||
|
state.operations += 1;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "unchecked"))]
|
||||||
|
{
|
||||||
|
// Guard against too many operations
|
||||||
|
if let Some(max) = self.max_operations {
|
||||||
|
if state.operations > max.get() {
|
||||||
|
return Err(Box::new(EvalAltResult::ErrorTooManyOperations(pos)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report progress - only in steps
|
||||||
|
if let Some(progress) = self.progress.as_ref() {
|
||||||
|
if !progress(state.operations) {
|
||||||
|
// Terminate script if progress returns false
|
||||||
|
return Err(Box::new(EvalAltResult::ErrorTerminated(pos)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Map a type_name into a pretty-print name
|
/// Map a type_name into a pretty-print name
|
||||||
pub(crate) fn map_type_name<'a>(&'a self, name: &'a str) -> &'a str {
|
pub(crate) fn map_type_name<'a>(&'a self, name: &'a str) -> &'a str {
|
||||||
self.type_names
|
self.type_names
|
||||||
|
@ -20,6 +20,11 @@ pub type PrintCallback = dyn Fn(&str) + Send + Sync + 'static;
|
|||||||
#[cfg(not(feature = "sync"))]
|
#[cfg(not(feature = "sync"))]
|
||||||
pub type PrintCallback = dyn Fn(&str) + 'static;
|
pub type PrintCallback = dyn Fn(&str) + 'static;
|
||||||
|
|
||||||
|
#[cfg(feature = "sync")]
|
||||||
|
pub type ProgressCallback = dyn Fn(u64) -> bool + Send + Sync + 'static;
|
||||||
|
#[cfg(not(feature = "sync"))]
|
||||||
|
pub type ProgressCallback = dyn Fn(u64) -> bool + 'static;
|
||||||
|
|
||||||
// Define callback function types
|
// Define callback function types
|
||||||
#[cfg(feature = "sync")]
|
#[cfg(feature = "sync")]
|
||||||
pub trait ObjectGetCallback<T, U>: Fn(&mut T) -> U + Send + Sync + 'static {}
|
pub trait ObjectGetCallback<T, U>: Fn(&mut T) -> U + Send + Sync + 'static {}
|
||||||
|
@ -79,8 +79,12 @@ pub enum EvalAltResult {
|
|||||||
ErrorDotExpr(String, Position),
|
ErrorDotExpr(String, Position),
|
||||||
/// Arithmetic error encountered. Wrapped value is the error message.
|
/// Arithmetic error encountered. Wrapped value is the error message.
|
||||||
ErrorArithmetic(String, Position),
|
ErrorArithmetic(String, Position),
|
||||||
|
/// Number of operations over maximum limit.
|
||||||
|
ErrorTooManyOperations(Position),
|
||||||
/// Call stack over maximum limit.
|
/// Call stack over maximum limit.
|
||||||
ErrorStackOverflow(Position),
|
ErrorStackOverflow(Position),
|
||||||
|
/// The script is prematurely terminated.
|
||||||
|
ErrorTerminated(Position),
|
||||||
/// Run-time error encountered. Wrapped value is the error message.
|
/// Run-time error encountered. Wrapped value is the error message.
|
||||||
ErrorRuntime(String, Position),
|
ErrorRuntime(String, Position),
|
||||||
|
|
||||||
@ -137,7 +141,9 @@ impl EvalAltResult {
|
|||||||
Self::ErrorInExpr(_) => "Malformed 'in' expression",
|
Self::ErrorInExpr(_) => "Malformed 'in' expression",
|
||||||
Self::ErrorDotExpr(_, _) => "Malformed dot expression",
|
Self::ErrorDotExpr(_, _) => "Malformed dot expression",
|
||||||
Self::ErrorArithmetic(_, _) => "Arithmetic error",
|
Self::ErrorArithmetic(_, _) => "Arithmetic error",
|
||||||
|
Self::ErrorTooManyOperations(_) => "Too many operations",
|
||||||
Self::ErrorStackOverflow(_) => "Stack overflow",
|
Self::ErrorStackOverflow(_) => "Stack overflow",
|
||||||
|
Self::ErrorTerminated(_) => "Script terminated.",
|
||||||
Self::ErrorRuntime(_, _) => "Runtime error",
|
Self::ErrorRuntime(_, _) => "Runtime error",
|
||||||
Self::ErrorLoopBreak(true, _) => "Break statement not inside a loop",
|
Self::ErrorLoopBreak(true, _) => "Break statement not inside a loop",
|
||||||
Self::ErrorLoopBreak(false, _) => "Continue statement not inside a loop",
|
Self::ErrorLoopBreak(false, _) => "Continue statement not inside a loop",
|
||||||
@ -183,7 +189,9 @@ impl fmt::Display for EvalAltResult {
|
|||||||
| Self::ErrorAssignmentToUnknownLHS(pos)
|
| Self::ErrorAssignmentToUnknownLHS(pos)
|
||||||
| Self::ErrorInExpr(pos)
|
| Self::ErrorInExpr(pos)
|
||||||
| Self::ErrorDotExpr(_, pos)
|
| Self::ErrorDotExpr(_, pos)
|
||||||
| Self::ErrorStackOverflow(pos) => write!(f, "{} ({})", desc, pos),
|
| Self::ErrorTooManyOperations(pos)
|
||||||
|
| Self::ErrorStackOverflow(pos)
|
||||||
|
| Self::ErrorTerminated(pos) => write!(f, "{} ({})", desc, pos),
|
||||||
|
|
||||||
Self::ErrorRuntime(s, pos) => {
|
Self::ErrorRuntime(s, pos) => {
|
||||||
write!(f, "{} ({})", if s.is_empty() { desc } else { s }, pos)
|
write!(f, "{} ({})", if s.is_empty() { desc } else { s }, pos)
|
||||||
@ -299,7 +307,9 @@ impl EvalAltResult {
|
|||||||
| Self::ErrorInExpr(pos)
|
| Self::ErrorInExpr(pos)
|
||||||
| Self::ErrorDotExpr(_, pos)
|
| Self::ErrorDotExpr(_, pos)
|
||||||
| Self::ErrorArithmetic(_, pos)
|
| Self::ErrorArithmetic(_, pos)
|
||||||
|
| Self::ErrorTooManyOperations(pos)
|
||||||
| Self::ErrorStackOverflow(pos)
|
| Self::ErrorStackOverflow(pos)
|
||||||
|
| Self::ErrorTerminated(pos)
|
||||||
| Self::ErrorRuntime(_, pos)
|
| Self::ErrorRuntime(_, pos)
|
||||||
| Self::ErrorLoopBreak(_, pos)
|
| Self::ErrorLoopBreak(_, pos)
|
||||||
| Self::Return(_, pos) => *pos,
|
| Self::Return(_, pos) => *pos,
|
||||||
@ -335,7 +345,9 @@ impl EvalAltResult {
|
|||||||
| Self::ErrorInExpr(pos)
|
| Self::ErrorInExpr(pos)
|
||||||
| Self::ErrorDotExpr(_, pos)
|
| Self::ErrorDotExpr(_, pos)
|
||||||
| Self::ErrorArithmetic(_, pos)
|
| Self::ErrorArithmetic(_, pos)
|
||||||
|
| Self::ErrorTooManyOperations(pos)
|
||||||
| Self::ErrorStackOverflow(pos)
|
| Self::ErrorStackOverflow(pos)
|
||||||
|
| Self::ErrorTerminated(pos)
|
||||||
| Self::ErrorRuntime(_, pos)
|
| Self::ErrorRuntime(_, pos)
|
||||||
| Self::ErrorLoopBreak(_, pos)
|
| Self::ErrorLoopBreak(_, pos)
|
||||||
| Self::Return(_, pos) => *pos = new_position,
|
| Self::Return(_, pos) => *pos = new_position,
|
||||||
|
93
tests/operations.rs
Normal file
93
tests/operations.rs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
#![cfg(not(feature = "unchecked"))]
|
||||||
|
use rhai::{Engine, EvalAltResult};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_max_operations() -> Result<(), Box<EvalAltResult>> {
|
||||||
|
let mut engine = Engine::new();
|
||||||
|
engine.set_max_operations(500);
|
||||||
|
|
||||||
|
engine.on_progress(|count| {
|
||||||
|
if count % 100 == 0 {
|
||||||
|
println!("{}", count);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
});
|
||||||
|
|
||||||
|
engine.eval::<()>("let x = 0; while x < 20 { x += 1; }")?;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
*engine
|
||||||
|
.eval::<()>("for x in range(0, 500) {}")
|
||||||
|
.expect_err("should error"),
|
||||||
|
EvalAltResult::ErrorTooManyOperations(_)
|
||||||
|
));
|
||||||
|
|
||||||
|
engine.set_max_operations(0);
|
||||||
|
|
||||||
|
engine.eval::<()>("for x in range(0, 10000) {}")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_max_operations_functions() -> Result<(), Box<EvalAltResult>> {
|
||||||
|
let mut engine = Engine::new();
|
||||||
|
engine.set_max_operations(500);
|
||||||
|
|
||||||
|
engine.on_progress(|count| {
|
||||||
|
if count % 100 == 0 {
|
||||||
|
println!("{}", count);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
});
|
||||||
|
|
||||||
|
engine.eval::<()>(
|
||||||
|
r#"
|
||||||
|
fn inc(x) { x + 1 }
|
||||||
|
let x = 0;
|
||||||
|
while x < 20 { x = inc(x); }
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
*engine
|
||||||
|
.eval::<()>(
|
||||||
|
r#"
|
||||||
|
fn inc(x) { x + 1 }
|
||||||
|
let x = 0;
|
||||||
|
while x < 1000 { x = inc(x); }
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.expect_err("should error"),
|
||||||
|
EvalAltResult::ErrorTooManyOperations(_)
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_max_operations_eval() -> Result<(), Box<EvalAltResult>> {
|
||||||
|
let mut engine = Engine::new();
|
||||||
|
engine.set_max_operations(500);
|
||||||
|
|
||||||
|
engine.on_progress(|count| {
|
||||||
|
if count % 100 == 0 {
|
||||||
|
println!("{}", count);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
*engine
|
||||||
|
.eval::<()>(
|
||||||
|
r#"
|
||||||
|
let script = "for x in range(0, 500) {}";
|
||||||
|
eval(script);
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.expect_err("should error"),
|
||||||
|
EvalAltResult::ErrorTooManyOperations(_)
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -15,6 +15,7 @@ fn test_stack_overflow() -> Result<(), Box<EvalAltResult>> {
|
|||||||
325
|
325
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "unchecked"))]
|
||||||
match engine.eval::<()>(
|
match engine.eval::<()>(
|
||||||
r"
|
r"
|
||||||
fn foo(n) { if n == 0 { 0 } else { n + foo(n-1) } }
|
fn foo(n) { if n == 0 { 0 } else { n + foo(n-1) } }
|
||||||
|
Loading…
Reference in New Issue
Block a user