diff --git a/README.md b/README.md
index 0b359294..831f3765 100644
--- a/README.md
+++ b/README.md
@@ -13,20 +13,25 @@ to add scripting to any application.
Rhai's current features set:
-* Easy-to-use language similar to JS+Rust
-* Easy integration with Rust [native functions](#working-with-functions) and [types](#custom-types-and-methods),
- including [getters/setters](#getters-and-setters), [methods](#members-and-methods) and [indexers](#indexers)
-* Easily [call a script-defined function](#calling-rhai-functions-from-rust) from Rust
-* Freely pass variables/constants into a script via an external [`Scope`]
-* 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)
-* Relatively little `unsafe` code (yes there are some for performance reasons)
-* [`no-std`](#optional-features) support
-* [Function overloading](#function-overloading)
-* [Operator overloading](#operator-overloading)
-* [Modules]
-* Compiled script is [optimized](#script-optimization) for repeat evaluations
-* Support for [minimal builds](#minimal-builds) by excluding unneeded language [features](#optional-features)
+* Easy-to-use language similar to JS+Rust with dynamic typing but _no_ garbage collector.
+* Tight integration with native Rust [functions](#working-with-functions) and [types](#custom-types-and-methods),
+ including [getters/setters](#getters-and-setters), [methods](#members-and-methods) and [indexers](#indexers).
+* Freely pass Rust variables/constants into a script via an external [`Scope`].
+* Easily [call a script-defined function](#calling-rhai-functions-from-rust) from Rust.
+* Low compile-time overhead (~0.6 sec debug/~3 sec release for `rhai_runner` sample app).
+* Fairly efficient evaluation (1 million iterations in 0.75 sec on my 5 year old laptop).
+* Relatively little `unsafe` code (yes there are some for performance reasons, and most `unsafe` code is limited to
+ one single source file, all with names starting with `"unsafe_"`).
+* Re-entrant scripting [`Engine`] can be made `Send + Sync` (via the [`sync`] feature).
+* Sand-boxed - the scripting [`Engine`], if declared immutable, cannot mutate the containing environment without explicit permission.
+* Rugged (protection against [stack-overflow](#maximum-stack-depth) and [runaway scripts](#maximum-number-of-operations) etc.).
+* Track script evaluation [progress](#tracking-progress) and manually terminate a script run.
+* [`no-std`](#optional-features) support.
+* [Function overloading](#function-overloading).
+* [Operator overloading](#operator-overloading).
+* Organize code base with dynamically-loadable [Modules].
+* Compiled script is [optimized](#script-optimization) for repeated evaluations.
+* 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/)
to do checked arithmetic operations); for [`no-std`](#optional-features) builds, a number of additional dependencies are
pulled in to provide for functionalities that used to be in `std`.
@@ -63,23 +68,24 @@ Beware that in order to use pre-releases (e.g. alpha and beta), the exact versio
Optional features
-----------------
-| Feature | Description |
-| ------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
-| `unchecked` | Exclude arithmetic checking (such as overflows and division by zero). Beware that a bad script may panic the entire system! |
-| `no_function` | Disable script-defined functions if not needed. |
-| `no_index` | Disable [arrays] and indexing features if not needed. |
-| `no_object` | Disable support for custom types and objects. |
-| `no_float` | Disable floating-point numbers and math if not needed. |
-| `no_optimize` | Disable the script optimizer. |
-| `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_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. |
-| `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, [`Engine`], [`Scope`] and `AST` are all `Send + Sync`. |
+| Feature | Description |
+| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `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. |
+| `no_index` | Disable [arrays] and indexing features. |
+| `no_object` | Disable support for custom types and object maps. |
+| `no_float` | Disable floating-point numbers and math. |
+| `no_optimize` | Disable the script optimizer. |
+| `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_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. |
+| `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, all Rhai types, including [`Engine`], [`Scope`] and `AST`, are all `Send + Sync`. |
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.
-Excluding unneeded functionalities can result in smaller, faster builds as well as less bugs due to a more restricted language.
+Excluding unneeded functionalities can result in smaller, faster builds
+as well as more control over what a script can (or cannot) do.
[`unchecked`]: #optional-features
[`no_index`]: #optional-features
@@ -105,7 +111,7 @@ requiring more CPU cycles to complete.
Also, turning on `no_float`, and `only_i32` makes the key [`Dynamic`] data type only 8 bytes small on 32-bit targets
while normally it can be up to 16 bytes (e.g. on x86/x64 CPU's) in order to hold an `i64` or `f64`.
-Making [`Dynamic`] small helps performance due to more caching efficiency.
+Making [`Dynamic`] small helps performance due to better cache efficiency.
### Minimal builds
@@ -114,6 +120,8 @@ the correct linker flags are used in `cargo.toml`:
```toml
[profile.release]
+lto = "fat" # turn on Link-Time Optimizations
+codegen-units = 1 # trade compile time with maximum optimization
opt-level = "z" # optimize for size
```
@@ -269,7 +277,7 @@ let ast = engine.compile_file("hello_world.rhai".into())?;
### 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]).
```rust
@@ -372,7 +380,7 @@ Use `Engine::new_raw` to create a _raw_ `Engine`, in which _nothing_ is added, n
### 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 to be used.
@@ -411,7 +419,7 @@ Therefore, a package only has to be created _once_.
Packages are actually implemented as [modules], so they share a lot of behavior and characteristics.
The main difference is that a package loads under the _global_ namespace, while a module loads under its own
-namespace alias specified in an `import` statement (see also [modules]).
+namespace alias specified in an [`import`] statement (see also [modules]).
A package is _static_ (i.e. pre-loaded into an [`Engine`]), while a module is _dynamic_ (i.e. loaded with
the `import` statement).
@@ -759,7 +767,7 @@ Because they [_short-circuit_](#boolean-operators), `&&` and `||` are handled sp
overriding them has no effect at all.
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.
```rust
@@ -963,7 +971,7 @@ Indexers
--------
Custom types can also expose an _indexer_ by registering an indexer function.
-A custom with an indexer function defined can use the bracket '`[]`' notation to get a property value
+A custom type with an indexer function defined can use the bracket '`[]`' notation to get a property value
(but not update it - indexers are read-only).
```rust
@@ -2043,7 +2051,7 @@ debug("world!"); // prints "world!" to stdout using debug formatting
### Overriding `print` and `debug` with callback functions
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
// Any function or closure that takes an '&str' argument can be used to override
@@ -2107,6 +2115,8 @@ export x as answer; // the variable 'x' is exported under the alias 'ans
### Importing modules
+[`import`]: #importing-modules
+
A module can be _imported_ via the `import` statement, and its members are accessed via '`::`' similar to C++.
```rust
@@ -2237,7 +2247,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.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`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. |
-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
// Use the 'StaticModuleResolver'
@@ -2248,6 +2258,128 @@ engine.set_module_resolver(Some(resolver));
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 consume more resources that it is allowed to.
+
+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 which is waiting for a result.
+* **Stack**: A malignant script may attempt an infinite recursive call that exhausts the call stack.
+* **Overflows**: A malignant script may deliberately cause numeric over-flows and/or under-flows, divide by zero, and/or
+ create 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).
+ Even when modules are not created from files, they still typically consume a lot of resources to load.
+* **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,
+loading one variable/constant, one operator call, one iteration of a loop, or one function call etc.
+with sub-expressions, statements and function calls executed inside these contexts accumulated on top.
+A good rule-of-thumb is that one simple non-trivial expression consumes on average 5-10 operations.
+
+One _operation_ can take an unspecified amount of time and real CPU cycles, depending on the particulars.
+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 computing resources.
+If it helps to visualize, think of an _operation_ as roughly equals to one _instruction_ of a hypothetical CPU.
+
+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 number of modules
+
+Rhai by default does not limit how many [modules] are loaded via the [`import`] statement.
+This can be changed via the `Engine::set_max_modules` method, with zero being unlimited (the default).
+
+```rust
+let mut engine = Engine::new();
+
+engine.set_max_modules(5); // allow loading only up to 5 modules
+
+engine.set_max_modules(0); // allow unlimited modules
+```
+
+### 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).
+
+### Blocking 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
===================
@@ -2358,7 +2490,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.
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
// Turn on aggressive optimizations
@@ -2536,7 +2668,7 @@ let x = eval("40 + 2"); // 'eval' here throws "eval is evil! I refuse to run
Or override it from Rust:
```rust
-fn alt_eval(script: String) -> Result<(), EvalAltResult> {
+fn alt_eval(script: String) -> Result<(), Box> {
Err(format!("eval is evil! I refuse to run {}", script).into())
}
diff --git a/RELEASES.md b/RELEASES.md
new file mode 100644
index 00000000..cee016d7
--- /dev/null
+++ b/RELEASES.md
@@ -0,0 +1,66 @@
+Rhai Release Notes
+==================
+
+Version 0.14.1
+==============
+
+The major features for this release is modules, script resource limits, and speed improvements
+(mainly due to avoiding allocations).
+
+New features
+------------
+
+* Modules and _module resolvers_ allow loading external scripts under a module namespace.
+ A module can contain constant variables, Rust functions and Rhai functions.
+* `export` variables and `private` functions.
+* _Indexers_ for Rust types.
+* Track script evaluation progress and terminate script run.
+* Set limit on maximum number of operations allowed per script run.
+* Set limit on maximum number of modules loaded per script run.
+* A new API, `Engine::compile_scripts_with_scope`, can compile a list of script segments without needing to
+ first concatenate them together into one large string.
+* Stepped `range` function with a custom step.
+
+Speed improvements
+------------------
+
+### `StaticVec`
+
+A script contains many lists - statements in a block, arguments to a function call etc.
+In a typical script, most of these lists tend to be short - e.g. the vast majority of function calls contain
+fewer than 4 arguments, while most statement blocks have fewer than 4-5 statements, with one or two being
+the most common. Before, dynamic `Vec`'s are used to hold these short lists for very brief periods of time,
+causing allocations churn.
+
+In this version, large amounts of allocations are avoided by converting to a `StaticVec` -
+a list type based on a static array for a small number of items (currently four) -
+wherever possible plus other tricks. Most real-life scripts should see material speed increases.
+
+### Pre-computed variable lookups
+
+Almost all variable lookups, as well as lookups in loaded modules, are now pre-computed.
+A variable's name is almost never used to search for the variable in the current scope.
+
+_Getters_ and _setter_ function names are also pre-computed and cached, so no string allocations are
+performed during a property get/set call.
+
+### Pre-computed function call hashes
+
+Lookup of all function calls, including Rust and Rhai ones, are now through pre-computed hashes.
+The function name is no longer used to search for a function, making function call dispatches
+much faster.
+
+### Large Boxes for expressions and statements
+
+The expression (`Expr`) and statement (`Stmt`) types are modified so that all of the variants contain only
+one single `Box` to a large allocated structure containing _all_ the fields. This makes the `Expr` and
+`Stmt` types very small (only one single pointer) and improves evaluation speed due to cache efficiency.
+
+Error handling
+--------------
+
+Previously, when an error occurs inside a function call, the error position reported is the function
+call site. This makes it difficult to diagnose the actual location of the error within the function.
+
+A new error variant `EvalAltResult::ErrorInFunctionCall` is added in this version.
+It wraps the internal error returned by the called function, including the error position within the function.
diff --git a/src/any.rs b/src/any.rs
index 483eee84..7f209685 100644
--- a/src/any.rs
+++ b/src/any.rs
@@ -19,7 +19,7 @@ use crate::stdlib::{
any::{type_name, Any, TypeId},
boxed::Box,
collections::HashMap,
- fmt, mem, ptr,
+ fmt,
string::String,
vec::Vec,
};
@@ -27,7 +27,7 @@ use crate::stdlib::{
#[cfg(not(feature = "no_std"))]
use crate::stdlib::time::Instant;
-/// A trait to represent any type.
+/// Trait to represent any type.
///
/// Currently, `Variant` is not `Send` nor `Sync`, so it can practically be any type.
/// Turn on the `sync` feature to restrict it to only types that implement `Send + Sync`.
@@ -81,7 +81,7 @@ impl Variant for T {
}
}
-/// A trait to represent any type.
+/// Trait to represent any type.
///
/// `From<_>` is implemented for `i64` (`i32` if `only_i32`), `f64` (if not `no_float`),
/// `bool`, `String`, `char`, `Vec` (into `Array`) and `HashMap` (into `Map`).
@@ -142,7 +142,7 @@ impl dyn Variant {
}
}
-/// A dynamic type containing any value.
+/// Dynamic type containing any value.
pub struct Dynamic(pub(crate) Union);
/// Internal `Dynamic` representation.
diff --git a/src/api.rs b/src/api.rs
index 24b28474..26a825ab 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -21,7 +21,6 @@ use crate::engine::Map;
use crate::stdlib::{
any::{type_name, TypeId},
boxed::Box,
- collections::HashMap,
mem,
string::{String, ToString},
};
@@ -653,7 +652,10 @@ impl Engine {
let scripts = [script];
let stream = lex(&scripts);
- parse_global_expr(&mut stream.peekable(), self, scope, self.optimization_level)
+ {
+ let mut peekable = stream.peekable();
+ parse_global_expr(&mut peekable, self, scope, self.optimization_level)
+ }
}
/// Evaluate a script file.
@@ -749,11 +751,10 @@ impl Engine {
scope: &mut Scope,
script: &str,
) -> Result> {
- // Since the AST will be thrown away afterwards, don't bother to optimize it
let ast = self.compile_with_scope_and_optimization_level(
scope,
&[script],
- OptimizationLevel::None,
+ self.optimization_level,
)?;
self.eval_ast_with_scope(scope, &ast)
}
@@ -865,7 +866,7 @@ impl Engine {
scope: &mut Scope,
ast: &AST,
) -> Result> {
- 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());
@@ -881,7 +882,7 @@ impl Engine {
&self,
scope: &mut Scope,
ast: &AST,
- ) -> Result> {
+ ) -> Result<(Dynamic, u64), Box> {
let mut state = State::new(ast.fn_lib());
ast.statements()
@@ -893,6 +894,7 @@ impl Engine {
EvalAltResult::Return(out, _) => Ok(out),
_ => Err(err),
})
+ .map(|v| (v, state.operations))
}
/// Evaluate a file, but throw away the result and only return error (if any).
@@ -1016,9 +1018,9 @@ impl Engine {
.ok_or_else(|| Box::new(EvalAltResult::ErrorFunctionNotFound(name.into(), pos)))?;
let state = State::new(fn_lib);
+ let args = args.as_mut();
- let result =
- self.call_script_fn(Some(scope), &state, name, fn_def, args.as_mut(), pos, 0)?;
+ let (result, _) = self.call_script_fn(Some(scope), state, name, fn_def, args, pos, 0)?;
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)
}
+ /// Register a callback for script evaluation progress.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box> {
+ /// # 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> {
+ /// # 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!`)
///
/// # Example
@@ -1092,21 +1172,21 @@ impl Engine {
///
/// ```
/// # fn main() -> Result<(), Box> {
- /// # use std::sync::RwLock;
- /// # use std::sync::Arc;
+ /// # use std::cell::RefCell;
+ /// # use std::rc::Rc;
/// use rhai::Engine;
///
- /// let result = Arc::new(RwLock::new(String::from("")));
+ /// let result = Rc::new(RefCell::new(String::from("")));
///
/// let mut engine = Engine::new();
///
/// // Override action of 'print' function
/// 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);")?;
///
- /// assert_eq!(*result.read().unwrap(), "42");
+ /// assert_eq!(*result.borrow(), "42");
/// # Ok(())
/// # }
/// ```
@@ -1149,21 +1229,21 @@ impl Engine {
///
/// ```
/// # fn main() -> Result<(), Box> {
- /// # use std::sync::RwLock;
- /// # use std::sync::Arc;
+ /// # use std::cell::RefCell;
+ /// # use std::rc::Rc;
/// use rhai::Engine;
///
- /// let result = Arc::new(RwLock::new(String::from("")));
+ /// let result = Rc::new(RefCell::new(String::from("")));
///
/// let mut engine = Engine::new();
///
/// // Override action of 'print' function
/// 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");"#)?;
///
- /// assert_eq!(*result.read().unwrap(), r#""hello""#);
+ /// assert_eq!(*result.borrow(), r#""hello""#);
/// # Ok(())
/// # }
/// ```
diff --git a/src/engine.rs b/src/engine.rs
index 7ef00d45..3febbc6f 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -3,12 +3,12 @@
use crate::any::{Dynamic, Union};
use crate::calc_fn_hash;
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::optimize::OptimizationLevel;
use crate::packages::{CorePackage, Package, PackageLibrary, PackagesCollection, StandardPackage};
use crate::parser::{Expr, FnAccess, FnDef, ReturnType, SharedFnDef, Stmt, AST};
-use crate::r#unsafe::unsafe_cast_var_name;
+use crate::r#unsafe::unsafe_cast_var_name_to_lifetime;
use crate::result::EvalAltResult;
use crate::scope::{EntryType as ScopeEntryType, Scope};
use crate::token::Position;
@@ -22,13 +22,12 @@ use crate::parser::ModuleRef;
use crate::stdlib::{
any::TypeId,
- borrow::Cow,
boxed::Box,
collections::HashMap,
format,
iter::{empty, once, repeat},
mem,
- num::NonZeroUsize,
+ num::{NonZeroU64, NonZeroUsize},
ops::{Deref, DerefMut},
rc::Rc,
string::{String, ToString},
@@ -36,24 +35,29 @@ use crate::stdlib::{
vec::Vec,
};
-/// An dynamic array of `Dynamic` values.
+/// Variable-sized array of `Dynamic` values.
///
/// Not available under the `no_index` feature.
#[cfg(not(feature = "no_index"))]
pub type Array = Vec;
-/// An dynamic hash map of `Dynamic` values with `String` keys.
+/// Hash map of `Dynamic` values with `String` keys.
///
/// Not available under the `no_object` feature.
#[cfg(not(feature = "no_object"))]
pub type Map = HashMap;
+#[cfg(not(feature = "unchecked"))]
#[cfg(debug_assertions)]
pub const MAX_CALL_STACK_DEPTH: usize = 28;
+#[cfg(not(feature = "unchecked"))]
#[cfg(not(debug_assertions))]
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_DEBUG: &str = "debug";
pub const KEYWORD_TYPE_OF: &str = "type_of";
@@ -68,19 +72,36 @@ enum Target<'a> {
/// The target is a mutable reference to a `Dynamic` value somewhere.
Ref(&'a mut Dynamic),
/// The target is a temporary `Dynamic` value (i.e. the mutation can cause no side effects).
- Value(Box),
+ Value(Dynamic),
/// The target is a character inside a String.
/// This is necessary because directly pointing to a char inside a String is impossible.
- StringChar(Box<(&'a mut Dynamic, usize, Dynamic)>),
+ StringChar(&'a mut Dynamic, usize, Dynamic),
}
impl Target<'_> {
- /// Get the value of the `Target` as a `Dynamic`.
+ /// Is the `Target` a reference pointing to other data?
+ pub fn is_ref(&self) -> bool {
+ match self {
+ Target::Ref(_) => true,
+ Target::Value(_) | Target::StringChar(_, _, _) => false,
+ }
+ }
+
+ /// Get the value of the `Target` as a `Dynamic`, cloning a referenced value if necessary.
pub fn clone_into_dynamic(self) -> Dynamic {
match self {
- Target::Ref(r) => r.clone(),
- Target::Value(v) => *v,
- Target::StringChar(s) => s.2,
+ Target::Ref(r) => r.clone(), // Referenced value is cloned
+ Target::Value(v) => v, // Owned value is simply taken
+ Target::StringChar(_, _, ch) => ch, // Character is taken
+ }
+ }
+
+ /// Get a mutable reference from the `Target`.
+ pub fn as_mut(&mut self) -> &mut Dynamic {
+ match self {
+ Target::Ref(r) => *r,
+ Target::Value(ref mut r) => r,
+ Target::StringChar(_, _, ref mut r) => r,
}
}
@@ -91,25 +112,23 @@ impl Target<'_> {
Target::Value(_) => {
return Err(Box::new(EvalAltResult::ErrorAssignmentToUnknownLHS(pos)))
}
- Target::StringChar(x) => match x.0 {
- Dynamic(Union::Str(s)) => {
- // Replace the character at the specified index position
- let new_ch = new_val
- .as_char()
- .map_err(|_| EvalAltResult::ErrorCharMismatch(pos))?;
+ Target::StringChar(Dynamic(Union::Str(s)), index, _) => {
+ // Replace the character at the specified index position
+ let new_ch = new_val
+ .as_char()
+ .map_err(|_| EvalAltResult::ErrorCharMismatch(pos))?;
- let mut chars: StaticVec = s.chars().collect();
- let ch = *chars.get_ref(x.1);
+ let mut chars: StaticVec = s.chars().collect();
+ let ch = chars[*index];
- // See if changed - if so, update the String
- if ch != new_ch {
- *chars.get_mut(x.1) = new_ch;
- s.clear();
- chars.iter().for_each(|&ch| s.push(ch));
- }
+ // See if changed - if so, update the String
+ if ch != new_ch {
+ chars[*index] = new_ch;
+ s.clear();
+ chars.iter().for_each(|&ch| s.push(ch));
}
- _ => unreachable!(),
- },
+ }
+ _ => unreachable!(),
}
Ok(())
@@ -123,7 +142,7 @@ impl<'a> From<&'a mut Dynamic> for Target<'a> {
}
impl> From for Target<'_> {
fn from(value: T) -> Self {
- Self::Value(Box::new(value.into()))
+ Self::Value(value.into())
}
}
@@ -146,15 +165,23 @@ pub struct State<'a> {
/// Level of the current scope. The global (root) level is zero, a new block (or function call)
/// is one level higher, and so on.
pub scope_level: usize,
+
+ /// Number of operations performed.
+ pub operations: u64,
+
+ /// Number of modules loaded.
+ pub modules: u64,
}
impl<'a> State<'a> {
/// Create a new `State`.
pub fn new(fn_lib: &'a FunctionsLib) -> Self {
Self {
- always_search: false,
fn_lib,
+ always_search: false,
scope_level: 0,
+ operations: 0,
+ modules: 0,
}
}
/// Does a certain script-defined function exist in the `State`?
@@ -163,7 +190,7 @@ impl<'a> State<'a> {
}
/// Get a script-defined function definition from the `State`.
pub fn get_function(&self, hash: u64) -> Option<&FnDef> {
- self.fn_lib.get(&hash).map(|f| f.as_ref())
+ self.fn_lib.get(&hash).map(|fn_def| fn_def.as_ref())
}
}
@@ -304,6 +331,8 @@ pub struct Engine {
pub(crate) print: Box,
/// Closure for implementing the `debug` command.
pub(crate) debug: Box,
+ /// Closure for progress reporting.
+ pub(crate) progress: Option>,
/// Optimize the AST after compilation.
pub(crate) optimization_level: OptimizationLevel,
@@ -311,6 +340,10 @@ pub struct Engine {
///
/// Defaults to 28 for debug builds and 256 for non-debug builds.
pub(crate) max_call_stack_depth: usize,
+ /// Maximum number of operations allowed to run.
+ pub(crate) max_operations: Option,
+ /// Maximum number of modules allowed to load.
+ pub(crate) max_modules: Option,
}
impl Default for Engine {
@@ -333,6 +366,9 @@ impl Default for Engine {
print: Box::new(default_print),
debug: Box::new(default_print),
+ // progress callback
+ progress: None,
+
// optimization level
#[cfg(feature = "no_optimize")]
optimization_level: OptimizationLevel::None,
@@ -346,6 +382,8 @@ impl Default for Engine {
optimization_level: OptimizationLevel::Full,
max_call_stack_depth: MAX_CALL_STACK_DEPTH,
+ max_operations: None,
+ max_modules: None,
};
#[cfg(feature = "no_stdlib")]
@@ -425,7 +463,7 @@ fn search_scope<'a>(
.downcast_mut::()
.unwrap()
} else {
- let (id, root_pos) = modules.get_ref(0);
+ let (id, root_pos) = modules.get(0);
scope.find_module(id).ok_or_else(|| {
Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos))
@@ -471,6 +509,7 @@ impl Engine {
type_names: Default::default(),
print: Box::new(|_| {}),
debug: Box::new(|_| {}),
+ progress: None,
#[cfg(feature = "no_optimize")]
optimization_level: OptimizationLevel::None,
@@ -484,6 +523,8 @@ impl Engine {
optimization_level: OptimizationLevel::Full,
max_call_stack_depth: MAX_CALL_STACK_DEPTH,
+ max_operations: None,
+ max_modules: None,
}
}
@@ -515,10 +556,24 @@ impl Engine {
/// Set the maximum levels of function calls allowed for a script in order to avoid
/// infinite recursion and stack overflows.
+ #[cfg(not(feature = "unchecked"))]
pub fn set_max_call_levels(&mut self, levels: usize) {
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 maximum number of imported modules allowed for a script (0 for unlimited).
+ #[cfg(not(feature = "unchecked"))]
+ pub fn set_max_modules(&mut self, modules: u64) {
+ self.max_modules = NonZeroU64::new(modules);
+ }
+
/// Set the module resolution service used by the `Engine`.
///
/// Not available under the `no_module` feature.
@@ -537,7 +592,7 @@ impl Engine {
pub(crate) fn call_fn_raw(
&self,
scope: Option<&mut Scope>,
- state: &State,
+ state: &mut State,
fn_name: &str,
hashes: (u64, u64),
args: &mut FnCallArgs,
@@ -546,6 +601,8 @@ impl Engine {
pos: Position,
level: usize,
) -> Result<(Dynamic, bool), Box> {
+ self.inc_operations(state, pos)?;
+
// Check for stack overflow
if level > self.max_call_stack_depth {
return Err(Box::new(EvalAltResult::ErrorStackOverflow(pos)));
@@ -554,9 +611,10 @@ impl Engine {
// First search in script-defined functions (can override built-in)
if hashes.1 > 0 {
if let Some(fn_def) = state.get_function(hashes.1) {
- return self
- .call_script_fn(scope, state, fn_name, fn_def, args, pos, level)
- .map(|v| (v, false));
+ let (result, state2) =
+ self.call_script_fn(scope, *state, fn_name, fn_def, args, pos, level)?;
+ *state = state2;
+ return Ok((result, false));
}
}
@@ -670,21 +728,21 @@ impl Engine {
/// **DO NOT** reuse the argument values unless for the first `&mut` argument - all others are silently replaced by `()`!
pub(crate) fn call_script_fn<'s>(
&self,
- scope: Option<&mut Scope<'s>>,
- state: &State,
+ scope: Option<&mut Scope>,
+ mut state: State<'s>,
fn_name: &str,
fn_def: &FnDef,
args: &mut FnCallArgs,
pos: Position,
level: usize,
- ) -> Result> {
+ ) -> Result<(Dynamic, State<'s>), Box> {
+ let orig_scope_level = state.scope_level;
+ state.scope_level += 1;
+
match scope {
// Extern scope passed in which is not empty
Some(scope) if scope.len() > 0 => {
let scope_len = scope.len();
- let mut state = State::new(state.fn_lib);
-
- state.scope_level += 1;
// Put arguments into scope as variables
scope.extend(
@@ -696,7 +754,8 @@ impl Engine {
args.into_iter().map(|v| mem::take(*v)),
)
.map(|(name, value)| {
- let var_name = unsafe_cast_var_name(name.as_str(), &state);
+ let var_name =
+ unsafe_cast_var_name_to_lifetime(name.as_str(), &mut state);
(var_name, ScopeEntryType::Normal, value)
}),
);
@@ -722,16 +781,14 @@ impl Engine {
});
// Remove all local variables
- // No need to reset `state.scope_level` because it is thrown away
scope.rewind(scope_len);
+ state.scope_level = orig_scope_level;
- return result;
+ return result.map(|v| (v, state));
}
// No new scope - create internal scope
_ => {
let mut scope = Scope::new();
- let mut state = State::new(state.fn_lib);
- state.scope_level += 1;
// Put arguments into scope as variables
scope.extend(
@@ -746,8 +803,7 @@ impl Engine {
);
// Evaluate the function at one higher level of call depth
- // No need to reset `state.scope_level` because it is thrown away
- return self
+ let result = self
.eval_stmt(&mut scope, &mut state, &fn_def.body, level + 1)
.or_else(|err| match *err {
// Convert return statement to return value
@@ -765,6 +821,9 @@ impl Engine {
pos,
))),
});
+
+ state.scope_level = orig_scope_level;
+ return result.map(|v| (v, state));
}
}
}
@@ -788,7 +847,7 @@ impl Engine {
/// **DO NOT** reuse the argument values unless for the first `&mut` argument - all others are silently replaced by `()`!
fn exec_fn_call(
&self,
- state: &State,
+ state: &mut State,
fn_name: &str,
hash_fn_def: u64,
args: &mut FnCallArgs,
@@ -827,7 +886,7 @@ impl Engine {
fn eval_script_expr(
&self,
scope: &mut Scope,
- state: &State,
+ state: &mut State,
script: &Dynamic,
pos: Position,
) -> Result> {
@@ -854,15 +913,21 @@ impl Engine {
let ast = AST::new(statements, state.fn_lib.clone());
// Evaluate the AST
- self.eval_ast_with_scope_raw(scope, &ast)
- .map_err(|err| err.new_position(pos))
+ let (result, operations) = self
+ .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.
fn eval_dot_index_chain_helper(
&self,
- state: &State,
- mut target: Target,
+ state: &mut State,
+ target: &mut Target,
rhs: &Expr,
idx_values: &mut StaticVec,
is_index: bool,
@@ -870,12 +935,10 @@ impl Engine {
level: usize,
mut new_val: Option,
) -> Result<(Dynamic, bool), Box> {
+ let is_ref = target.is_ref();
+
// Get a reference to the mutation target Dynamic
- let (obj, is_ref) = match target {
- Target::Ref(r) => (r, true),
- Target::Value(ref mut r) => (r.as_mut(), false),
- Target::StringChar(ref mut x) => (&mut x.2, false),
- };
+ let obj = target.as_mut();
// Pop the last index value
let mut idx_val = idx_values.pop();
@@ -886,20 +949,20 @@ impl Engine {
Expr::Dot(x) | Expr::Index(x) => {
let is_idx = matches!(rhs, Expr::Index(_));
let pos = x.0.position();
- let val =
- self.get_indexed_mut(state, obj, is_ref, idx_val, pos, op_pos, false)?;
+ let this_ptr = &mut self
+ .get_indexed_mut(state, obj, is_ref, idx_val, pos, op_pos, false)?;
self.eval_dot_index_chain_helper(
- state, val, &x.1, idx_values, is_idx, x.2, level, new_val,
+ state, this_ptr, &x.1, idx_values, is_idx, x.2, level, new_val,
)
}
// xxx[rhs] = new_val
_ if new_val.is_some() => {
let pos = rhs.position();
- let mut val =
- self.get_indexed_mut(state, obj, is_ref, idx_val, pos, op_pos, true)?;
+ let this_ptr = &mut self
+ .get_indexed_mut(state, obj, is_ref, idx_val, pos, op_pos, true)?;
- val.set_value(new_val.unwrap(), rhs.position())?;
+ this_ptr.set_value(new_val.unwrap(), rhs.position())?;
Ok((Default::default(), true))
}
// xxx[rhs]
@@ -968,7 +1031,7 @@ impl Engine {
Expr::Index(x) | Expr::Dot(x) if obj.is::