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::() => { let is_idx = matches!(rhs, Expr::Index(_)); - let val = if let Expr::Property(p) = &x.0 { + let mut val = if let Expr::Property(p) = &x.0 { let ((prop, _, _), _) = p.as_ref(); let index = prop.clone().into(); self.get_indexed_mut(state, obj, is_ref, index, x.2, op_pos, false)? @@ -981,7 +1044,7 @@ impl Engine { }; self.eval_dot_index_chain_helper( - state, val, &x.1, idx_values, is_idx, x.2, level, new_val, + state, &mut val, &x.1, idx_values, is_idx, x.2, level, new_val, ) } // xxx.idx_lhs[idx_expr] | xxx.dot_lhs.rhs @@ -1000,16 +1063,10 @@ impl Engine { ))); }; let val = &mut val; + let target = &mut val.into(); let (result, may_be_changed) = self.eval_dot_index_chain_helper( - state, - val.into(), - &x.1, - idx_values, - is_idx, - x.2, - level, - new_val, + state, target, &x.1, idx_values, is_idx, x.2, level, new_val, )?; // Feed the value back via a setter just in case it has been updated @@ -1061,6 +1118,7 @@ impl Engine { let index = if state.always_search { None } else { *index }; let mod_and_hash = modules.as_ref().map(|m| (m, *hash_var)); let (target, typ) = search_scope(scope, &name, mod_and_hash, index, *pos)?; + self.inc_operations(state, *pos)?; // Constants cannot be modified match typ { @@ -1074,7 +1132,7 @@ impl Engine { ScopeEntryType::Constant | ScopeEntryType::Normal => (), } - let this_ptr = target.into(); + let this_ptr = &mut target.into(); self.eval_dot_index_chain_helper( state, this_ptr, dot_rhs, idx_values, is_index, op_pos, level, new_val, ) @@ -1089,7 +1147,7 @@ impl Engine { // {expr}.??? or {expr}[???] expr => { let val = self.eval_expr(scope, state, expr, level)?; - let this_ptr = val.into(); + let this_ptr = &mut val.into(); self.eval_dot_index_chain_helper( state, this_ptr, dot_rhs, idx_values, is_index, op_pos, level, new_val, ) @@ -1112,13 +1170,14 @@ impl Engine { size: usize, level: usize, ) -> Result<(), Box> { + self.inc_operations(state, expr.position())?; + match expr { Expr::FnCall(x) if x.1.is_none() => { - let mut arg_values = StaticVec::::new(); - - for arg_expr in x.3.iter() { - arg_values.push(self.eval_expr(scope, state, arg_expr, level)?); - } + let arg_values = + x.3.iter() + .map(|arg_expr| self.eval_expr(scope, state, arg_expr, level)) + .collect::, _>>()?; idx_values.push(Dynamic::from(arg_values)); } @@ -1145,7 +1204,7 @@ impl Engine { /// Get the value at the indexed position of a base type fn get_indexed_mut<'a>( &self, - state: &State, + state: &mut State, val: &'a mut Dynamic, is_ref: bool, mut idx: Dynamic, @@ -1153,6 +1212,8 @@ impl Engine { op_pos: Position, create: bool, ) -> Result, Box> { + self.inc_operations(state, op_pos)?; + match val { #[cfg(not(feature = "no_index"))] Dynamic(Union::Array(arr)) => { @@ -1205,7 +1266,7 @@ impl Engine { let ch = s.chars().nth(offset).ok_or_else(|| { Box::new(EvalAltResult::ErrorStringBounds(chars_len, index, idx_pos)) })?; - Ok(Target::StringChar(Box::new((val, offset, ch.into())))) + Ok(Target::StringChar(val, offset, ch.into())) } else { Err(Box::new(EvalAltResult::ErrorStringBounds( chars_len, index, idx_pos, @@ -1234,6 +1295,8 @@ impl Engine { rhs: &Expr, level: usize, ) -> Result> { + self.inc_operations(state, rhs.position())?; + let mut lhs_value = self.eval_expr(scope, state, lhs, level)?; let rhs_value = self.eval_expr(scope, state, rhs, level)?; @@ -1266,13 +1329,13 @@ impl Engine { #[cfg(not(feature = "no_object"))] Dynamic(Union::Map(rhs_value)) => match lhs_value { // Only allows String or char - Dynamic(Union::Str(s)) => Ok(rhs_value.contains_key(s.as_ref()).into()), + Dynamic(Union::Str(s)) => Ok(rhs_value.contains_key(s.as_str()).into()), Dynamic(Union::Char(c)) => Ok(rhs_value.contains_key(&c.to_string()).into()), _ => Err(Box::new(EvalAltResult::ErrorInExpr(lhs.position()))), }, Dynamic(Union::Str(rhs_value)) => match lhs_value { // Only allows String or char - Dynamic(Union::Str(s)) => Ok(rhs_value.contains(s.as_ref()).into()), + Dynamic(Union::Str(s)) => Ok(rhs_value.contains(s.as_str()).into()), Dynamic(Union::Char(c)) => Ok(rhs_value.contains(c).into()), _ => Err(Box::new(EvalAltResult::ErrorInExpr(lhs.position()))), }, @@ -1288,6 +1351,8 @@ impl Engine { expr: &Expr, level: usize, ) -> Result> { + self.inc_operations(state, expr.position())?; + match expr { Expr::IntegerConstant(x) => Ok(x.0.into()), #[cfg(not(feature = "no_float"))] @@ -1318,6 +1383,8 @@ impl Engine { let index = if state.always_search { None } else { *index }; let mod_and_hash = modules.as_ref().map(|m| (m, *hash_var)); let (lhs_ptr, typ) = search_scope(scope, name, mod_and_hash, index, *pos)?; + self.inc_operations(state, *pos)?; + match typ { ScopeEntryType::Constant => Err(Box::new( EvalAltResult::ErrorAssignmentToConstant(name.clone(), *pos), @@ -1401,20 +1468,16 @@ impl Engine { let mut args: StaticVec<_> = arg_values.iter_mut().collect(); - if name == KEYWORD_EVAL && args.len() == 1 && args.get_ref(0).is::() { + if name == KEYWORD_EVAL && args.len() == 1 && args.get(0).is::() { let hash_fn = calc_fn_hash(empty(), name, once(TypeId::of::())); if !self.has_override(state, (hash_fn, *hash_fn_def)) { // eval - only in function call style let prev_len = scope.len(); + let pos = args_expr.get(0).position(); // Evaluate the text string as a script - let result = self.eval_script_expr( - scope, - state, - args.pop(), - args_expr[0].position(), - ); + let result = self.eval_script_expr(scope, state, args.pop(), pos); if scope.len() != prev_len { // IMPORTANT! If the eval defines new variables in the current scope, @@ -1445,7 +1508,7 @@ impl Engine { let mut args: StaticVec<_> = arg_values.iter_mut().collect(); - let (id, root_pos) = modules.get_ref(0); // First module + let (id, root_pos) = modules.get(0); // First module let module = if let Some(index) = modules.index() { scope @@ -1461,9 +1524,14 @@ impl Engine { // First search in script-defined functions (can override built-in) 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 (result, state2) = + self.call_script_fn(None, *state, name, fn_def, args, *pos, level)?; + *state = state2; + Ok(result) } else { // Then search in Rust functions + self.inc_operations(state, *pos)?; // Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, @@ -1538,6 +1606,8 @@ impl Engine { stmt: &Stmt, level: usize, ) -> Result> { + self.inc_operations(state, stmt.position())?; + match stmt { // No-op Stmt::Noop(_) => Ok(Default::default()), @@ -1546,11 +1616,10 @@ impl Engine { Stmt::Expr(expr) => { 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 - Default::default() - } else { - result + Expr::Assignment(_) => Default::default(), + _ => result, }) } @@ -1574,24 +1643,29 @@ impl Engine { } // If-else statement - Stmt::IfThenElse(x) => self - .eval_expr(scope, state, &x.0, level)? - .as_bool() - .map_err(|_| Box::new(EvalAltResult::ErrorLogicGuard(x.0.position()))) - .and_then(|guard_val| { - if guard_val { - self.eval_stmt(scope, state, &x.1, level) - } else if let Some(stmt) = &x.2 { - self.eval_stmt(scope, state, stmt, level) - } else { - Ok(Default::default()) - } - }), + Stmt::IfThenElse(x) => { + let (expr, if_block, else_block) = x.as_ref(); + + self.eval_expr(scope, state, expr, level)? + .as_bool() + .map_err(|_| Box::new(EvalAltResult::ErrorLogicGuard(expr.position()))) + .and_then(|guard_val| { + if guard_val { + self.eval_stmt(scope, state, if_block, level) + } else if let Some(stmt) = else_block { + self.eval_stmt(scope, state, stmt, level) + } else { + Ok(Default::default()) + } + }) + } // While loop Stmt::While(x) => loop { - match self.eval_expr(scope, state, &x.0, level)?.as_bool() { - Ok(true) => match self.eval_stmt(scope, state, &x.1, level) { + let (expr, body) = x.as_ref(); + + match self.eval_expr(scope, state, expr, level)?.as_bool() { + Ok(true) => match self.eval_stmt(scope, state, body, level) { Ok(_) => (), Err(err) => match *err { EvalAltResult::ErrorLoopBreak(false, _) => (), @@ -1600,7 +1674,9 @@ impl Engine { }, }, 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()))) + } } }, @@ -1618,7 +1694,8 @@ impl Engine { // For loop 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(); if let Some(iter_fn) = self @@ -1627,15 +1704,16 @@ impl Engine { .or_else(|| self.packages.get_iter(tid)) { // Add the loop variable - let var_name = unsafe_cast_var_name(&x.0, &state); + let var_name = unsafe_cast_var_name_to_lifetime(name, &state); scope.push(var_name, ()); let index = scope.len() - 1; state.scope_level += 1; for loop_var in iter_fn(iter_type) { *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(_) => (), Err(err) => match *err { EvalAltResult::ErrorLoopBreak(false, _) => (), @@ -1692,14 +1770,14 @@ impl Engine { Stmt::Let(x) if x.1.is_some() => { let ((var_name, _), expr) = x.as_ref(); 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_to_lifetime(var_name, &state); scope.push_dynamic_value(var_name, ScopeEntryType::Normal, val, false); Ok(Default::default()) } Stmt::Let(x) => { let ((var_name, _), _) = x.as_ref(); - let var_name = unsafe_cast_var_name(var_name, &state); + let var_name = unsafe_cast_var_name_to_lifetime(var_name, &state); scope.push(var_name, ()); Ok(Default::default()) } @@ -1708,7 +1786,7 @@ impl Engine { Stmt::Const(x) if x.1.is_constant() => { let ((var_name, _), expr) = x.as_ref(); 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_to_lifetime(var_name, &state); scope.push_dynamic_value(var_name, ScopeEntryType::Constant, val, true); Ok(Default::default()) } @@ -1718,24 +1796,34 @@ impl Engine { // Import statement Stmt::Import(x) => { - let (expr, (name, _)) = x.as_ref(); - #[cfg(feature = "no_module")] unreachable!(); #[cfg(not(feature = "no_module"))] { + let (expr, (name, pos)) = x.as_ref(); + + // Guard against too many modules + if let Some(max) = self.max_modules { + if state.modules >= max.get() { + return Err(Box::new(EvalAltResult::ErrorTooManyModules(*pos))); + } + } + if let Some(path) = self .eval_expr(scope, state, &expr, level)? .try_cast::() { - if let Some(resolver) = self.module_resolver.as_ref() { + if let Some(resolver) = &self.module_resolver { // Use an empty scope to create a module let module = resolver.resolve(self, Scope::new(), &path, expr.position())?; - let mod_name = unsafe_cast_var_name(name, &state); + let mod_name = unsafe_cast_var_name_to_lifetime(name, &state); scope.push_module(mod_name, module); + + state.modules += 1; + Ok(Default::default()) } else { Err(Box::new(EvalAltResult::ErrorModuleNotFound( @@ -1751,7 +1839,7 @@ impl Engine { // Export statement Stmt::Export(list) => { - for ((id, id_pos), rename) in list.as_ref() { + for ((id, id_pos), rename) in list.iter() { // Mark scope variables as public if let Some(index) = scope .get_index(id) @@ -1776,6 +1864,31 @@ impl Engine { } } + /// Check if the number of operations stay within limit. + fn inc_operations(&self, state: &mut State, pos: Position) -> Result<(), Box> { + 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 { + 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 pub(crate) fn map_type_name<'a>(&'a self, name: &'a str) -> &'a str { self.type_names diff --git a/src/fn_call.rs b/src/fn_call.rs index e1da767b..58711ebb 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -9,7 +9,7 @@ use crate::utils::StaticVec; /// Any data type that can be converted into a `Vec` can be used /// as arguments to a function call. pub trait FuncArgs { - /// Convert to a `Vec` of the function call arguments. + /// Convert to a `StaticVec` of the function call arguments. fn into_vec(self) -> StaticVec; } diff --git a/src/fn_func.rs b/src/fn_func.rs index e619ebdf..f606b794 100644 --- a/src/fn_func.rs +++ b/src/fn_func.rs @@ -11,7 +11,7 @@ use crate::scope::Scope; use crate::stdlib::{boxed::Box, string::ToString}; -/// A trait to create a Rust anonymous function from a script. +/// Trait to create a Rust anonymous function from a script. pub trait Func { type Output; diff --git a/src/fn_native.rs b/src/fn_native.rs index a8daac5f..7a1dd0e6 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -20,6 +20,11 @@ pub type PrintCallback = dyn Fn(&str) + Send + Sync + 'static; #[cfg(not(feature = "sync"))] 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 #[cfg(feature = "sync")] pub trait ObjectGetCallback: Fn(&mut T) -> U + Send + Sync + 'static {} @@ -77,7 +82,7 @@ pub enum NativeFunctionABI { Method, } -/// A trait implemented by all native Rust functions that are callable by Rhai. +/// Trait implemented by all native Rust functions that are callable by Rhai. #[cfg(not(feature = "sync"))] pub trait NativeCallable { /// Get the ABI type of a native Rust function. @@ -86,7 +91,7 @@ pub trait NativeCallable { fn call(&self, args: &mut FnCallArgs) -> Result>; } -/// A trait implemented by all native Rust functions that are callable by Rhai. +/// Trait implemented by all native Rust functions that are callable by Rhai. #[cfg(feature = "sync")] pub trait NativeCallable: Send + Sync { /// Get the ABI type of a native Rust function. diff --git a/src/fn_register.rs b/src/fn_register.rs index 8b91ea42..42e81c9b 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -10,7 +10,7 @@ use crate::result::EvalAltResult; use crate::stdlib::{any::TypeId, boxed::Box, mem, string::ToString}; -/// A trait to register custom functions with the `Engine`. +/// Trait to register custom functions with the `Engine`. pub trait RegisterFn { /// Register a custom function with the `Engine`. /// @@ -42,7 +42,7 @@ pub trait RegisterFn { fn register_fn(&mut self, name: &str, f: FN); } -/// A trait to register custom functions that return `Dynamic` values with the `Engine`. +/// Trait to register custom functions that return `Dynamic` values with the `Engine`. pub trait RegisterDynamicFn { /// Register a custom function returning `Dynamic` values with the `Engine`. /// @@ -69,7 +69,7 @@ pub trait RegisterDynamicFn { fn register_dynamic_fn(&mut self, name: &str, f: FN); } -/// A trait to register fallible custom functions returning `Result<_, Box>` with the `Engine`. +/// Trait to register fallible custom functions returning `Result<_, Box>` with the `Engine`. pub trait RegisterResultFn { /// Register a custom fallible function with the `Engine`. /// diff --git a/src/lib.rs b/src/lib.rs index f5f1ad87..5c7a10ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,7 +91,6 @@ mod utils; pub use any::Dynamic; pub use engine::Engine; pub use error::{ParseError, ParseErrorType}; -pub use fn_call::FuncArgs; pub use fn_native::NativeCallable; pub use fn_register::{RegisterDynamicFn, RegisterFn, RegisterResultFn}; pub use module::Module; @@ -116,6 +115,7 @@ pub use parser::FLOAT; #[cfg(not(feature = "no_module"))] pub use module::ModuleResolver; +/// Module containing all built-in _module resolvers_ available to Rhai. #[cfg(not(feature = "no_module"))] pub mod module_resolvers { pub use crate::module::resolvers::*; diff --git a/src/module.rs b/src/module.rs index 6e56a128..377a4357 100644 --- a/src/module.rs +++ b/src/module.rs @@ -51,7 +51,7 @@ pub struct Module { all_variables: HashMap, /// External Rust functions. - functions: HashMap, SharedNativeFunction)>, + functions: HashMap, SharedNativeFunction)>, /// Flattened collection of all external Rust functions, including those in sub-modules. all_functions: HashMap, @@ -292,8 +292,9 @@ impl Module { #[cfg(feature = "sync")] let func = Arc::new(f); - self.functions - .insert(hash_fn, (name, access, params.to_vec(), func)); + let params = params.into_iter().cloned().collect(); + + self.functions.insert(hash_fn, (name, access, params, func)); hash_fn } @@ -616,13 +617,13 @@ impl Module { pub(crate) fn index_all_sub_modules(&mut self) { // Collect a particular module. fn index_module<'a>( - module: &'a mut Module, + module: &'a Module, qualifiers: &mut Vec<&'a str>, variables: &mut Vec<(u64, Dynamic)>, functions: &mut Vec<(u64, SharedNativeFunction)>, fn_lib: &mut Vec<(u64, SharedFnDef)>, ) { - for (name, m) in module.modules.iter_mut() { + for (name, m) in &module.modules { // Index all the sub-modules first. qualifiers.push(name); index_module(m, qualifiers, variables, functions, fn_lib); @@ -630,7 +631,7 @@ impl Module { } // Index all variables - for (var_name, value) in module.variables.iter() { + for (var_name, value) in &module.variables { // Qualifiers + variable name let hash_var = calc_fn_hash(qualifiers.iter().map(|&v| v), var_name, empty()); variables.push((hash_var, value.clone())); @@ -716,7 +717,7 @@ impl Module { /// /// A `StaticVec` is used because most module-level access contains only one level, /// and it is wasteful to always allocate a `Vec` with one element. -#[derive(Clone, Eq, PartialEq, Hash, Default)] +#[derive(Clone, Eq, PartialEq, Default)] pub struct ModuleRef(StaticVec<(String, Position)>, Option); impl fmt::Debug for ModuleRef { @@ -769,7 +770,7 @@ impl ModuleRef { } } -/// A trait that encapsulates a module resolution service. +/// Trait that encapsulates a module resolution service. #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "sync"))] pub trait ModuleResolver { @@ -783,7 +784,7 @@ pub trait ModuleResolver { ) -> Result>; } -/// A trait that encapsulates a module resolution service. +/// Trait that encapsulates a module resolution service. #[cfg(not(feature = "no_module"))] #[cfg(feature = "sync")] pub trait ModuleResolver: Send + Sync { @@ -812,7 +813,7 @@ mod file { use super::*; use crate::stdlib::path::PathBuf; - /// A module resolution service that loads module script files from the file system. + /// Module resolution service that loads module script files from the file system. /// /// The `new_with_path` and `new_with_path_and_extension` constructor functions /// allow specification of a base directory with module path used as a relative path offset @@ -949,7 +950,7 @@ mod file { mod stat { use super::*; - /// A module resolution service that serves modules added into it. + /// Module resolution service that serves modules added into it. /// /// # Examples /// diff --git a/src/optimize.rs b/src/optimize.rs index df385b74..671415e3 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -10,11 +10,12 @@ use crate::parser::{map_dynamic_to_expr, Expr, FnDef, ReturnType, Stmt, AST}; use crate::result::EvalAltResult; use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; use crate::token::Position; +use crate::utils::StaticVec; use crate::stdlib::{ boxed::Box, - collections::HashMap, iter::empty, + mem, string::{String, ToString}, vec, vec::Vec, @@ -141,7 +142,11 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - if preserve_result { // -> { expr, Noop } - Stmt::Block(Box::new((vec![Stmt::Expr(Box::new(expr)), x.1], pos))) + let mut statements = StaticVec::new(); + statements.push(Stmt::Expr(Box::new(expr))); + statements.push(x.1); + + Stmt::Block(Box::new((statements, pos))) } else { // -> expr Stmt::Expr(Box::new(expr)) @@ -194,7 +199,8 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - Stmt::Break(pos) => { // Only a single break statement - turn into running the guard expression once state.set_dirty(); - let mut statements = vec![Stmt::Expr(Box::new(optimize_expr(expr, state)))]; + let mut statements = StaticVec::new(); + statements.push(Stmt::Expr(Box::new(optimize_expr(expr, state)))); if preserve_result { statements.push(Stmt::Noop(pos)) } @@ -325,13 +331,13 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - Stmt::Noop(pos) } // Only one let/import statement - leave it alone - [Stmt::Let(_)] | [Stmt::Import(_)] => Stmt::Block(Box::new((result, pos))), + [Stmt::Let(_)] | [Stmt::Import(_)] => Stmt::Block(Box::new((result.into(), pos))), // Only one statement - promote [_] => { state.set_dirty(); result.remove(0) } - _ => Stmt::Block(Box::new((result, pos))), + _ => Stmt::Block(Box::new((result.into(), pos))), } } // expr; @@ -417,7 +423,7 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { // Array literal where everything is pure - promote the indexed item. // All other items can be thrown away. state.set_dirty(); - a.0.remove(i.0 as usize).set_position(a.1) + a.0.take(i.0 as usize).set_position(a.1) } // map[string] (Expr::Map(m), Expr::StringConstant(s)) if m.0.iter().all(|(_, x)| x.is_pure()) => { @@ -441,14 +447,12 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { // [ items .. ] #[cfg(not(feature = "no_index"))] Expr::Array(a) => Expr::Array(Box::new((a.0 - .into_iter() - .map(|expr| optimize_expr(expr, state)) - .collect(), a.1))), + .into_iter().map(|expr| optimize_expr(expr, state)) + .collect(), a.1))), // [ items .. ] #[cfg(not(feature = "no_object"))] Expr::Map(m) => Expr::Map(Box::new((m.0 - .into_iter() - .map(|((key, pos), expr)| ((key, pos), optimize_expr(expr, state))) + .into_iter().map(|((key, pos), expr)| ((key, pos), optimize_expr(expr, state))) .collect(), m.1))), // lhs in rhs Expr::In(x) => match (x.0, x.1) { @@ -547,8 +551,8 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { return Expr::FnCall(x); } - let mut arg_values: Vec<_> = args.iter().map(Expr::get_constant_value).collect(); - let mut call_args: Vec<_> = arg_values.iter_mut().collect(); + let mut arg_values: StaticVec<_> = args.iter().map(Expr::get_constant_value).collect(); + let mut call_args: StaticVec<_> = arg_values.iter_mut().collect(); // Save the typename of the first argument if it is `type_of()` // This is to avoid `call_args` being passed into the closure @@ -558,7 +562,7 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { "" }; - call_fn(&state.engine.packages, &state.engine.global_module, name, &mut call_args, *pos).ok() + call_fn(&state.engine.packages, &state.engine.global_module, name, call_args.as_mut(), *pos).ok() .and_then(|result| result.or_else(|| { if !arg_for_type_of.is_empty() { @@ -698,11 +702,14 @@ pub fn optimize_into_ast( const level: OptimizationLevel = OptimizationLevel::None; #[cfg(not(feature = "no_function"))] - let fn_lib: Vec<_> = functions + let fn_lib_values: StaticVec<_> = functions .iter() .map(|fn_def| (fn_def.name.as_str(), fn_def.params.len())) .collect(); + #[cfg(not(feature = "no_function"))] + let fn_lib = fn_lib_values.as_ref(); + #[cfg(feature = "no_function")] const fn_lib: &[(&str, usize)] = &[]; @@ -712,7 +719,7 @@ pub fn optimize_into_ast( let pos = fn_def.body.position(); // Optimize the function body - let mut body = optimize(vec![fn_def.body], engine, &Scope::new(), &fn_lib, level); + let mut body = optimize(vec![fn_def.body], engine, &Scope::new(), fn_lib, level); // {} -> Noop fn_def.body = match body.pop().unwrap_or_else(|| Stmt::Noop(pos)) { @@ -738,7 +745,7 @@ pub fn optimize_into_ast( match level { OptimizationLevel::None => statements, OptimizationLevel::Simple | OptimizationLevel::Full => { - optimize(statements, engine, &scope, &fn_lib, level) + optimize(statements, engine, &scope, fn_lib, level) } }, lib, diff --git a/src/packages/mod.rs b/src/packages/mod.rs index 5c2a6dd5..e56843f0 100644 --- a/src/packages/mod.rs +++ b/src/packages/mod.rs @@ -1,7 +1,8 @@ -//! This module contains all built-in _packages_ available to Rhai, plus facilities to define custom packages. +//! Module containing all built-in _packages_ available to Rhai, plus facilities to define custom packages. use crate::fn_native::{NativeCallable, SharedIteratorFunction}; use crate::module::Module; +use crate::utils::StaticVec; use crate::stdlib::{any::TypeId, boxed::Box, collections::HashMap, rc::Rc, sync::Arc, vec::Vec}; @@ -54,7 +55,7 @@ pub type PackageLibrary = Arc; #[derive(Clone, Default)] pub(crate) struct PackagesCollection { /// Collection of `PackageLibrary` instances. - packages: Vec, + packages: StaticVec, } impl PackagesCollection { @@ -89,7 +90,7 @@ impl PackagesCollection { } } -/// This macro makes it easy to define a _package_ (which is basically a shared module) +/// Macro that makes it easy to define a _package_ (which is basically a shared module) /// and register functions into it. /// /// Functions can be added to the package using the standard module methods such as diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index eb6208f1..2c90bea5 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -1,6 +1,7 @@ use crate::def_package; use crate::module::FuncReturn; use crate::parser::INT; +use crate::utils::StaticVec; #[cfg(not(feature = "no_index"))] use crate::engine::Array; @@ -29,7 +30,7 @@ fn sub_string(s: &mut String, start: INT, len: INT) -> FuncReturn { start as usize }; - let chars: Vec<_> = s.chars().collect(); + let chars: StaticVec<_> = s.chars().collect(); let len = if offset + (len as usize) > chars.len() { chars.len() - offset @@ -37,7 +38,7 @@ fn sub_string(s: &mut String, start: INT, len: INT) -> FuncReturn { len as usize }; - Ok(chars[offset..][..len].into_iter().collect()) + Ok(chars.iter().skip(offset).take(len).cloned().collect()) } fn crop_string(s: &mut String, start: INT, len: INT) -> FuncReturn<()> { let offset = if s.is_empty() || len <= 0 { @@ -52,7 +53,7 @@ fn crop_string(s: &mut String, start: INT, len: INT) -> FuncReturn<()> { start as usize }; - let chars: Vec<_> = s.chars().collect(); + let chars: StaticVec<_> = s.chars().collect(); let len = if offset + (len as usize) > chars.len() { chars.len() - offset @@ -62,8 +63,10 @@ fn crop_string(s: &mut String, start: INT, len: INT) -> FuncReturn<()> { s.clear(); - chars[offset..][..len] - .into_iter() + chars + .iter() + .skip(offset) + .take(len) .for_each(|&ch| s.push(ch)); Ok(()) @@ -189,9 +192,9 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str "truncate", |s: &mut String, len: INT| { if len >= 0 { - let chars: Vec<_> = s.chars().take(len as usize).collect(); + let chars: StaticVec<_> = s.chars().take(len as usize).collect(); s.clear(); - chars.into_iter().for_each(|ch| s.push(ch)); + chars.iter().for_each(|&ch| s.push(ch)); } else { s.clear(); } diff --git a/src/parser.rs b/src/parser.rs index 0fefc84c..29c955e2 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -7,7 +7,7 @@ use crate::error::{LexError, ParseError, ParseErrorType}; use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::scope::{EntryType as ScopeEntryType, Scope}; use crate::token::{Position, Token, TokenIterator}; -use crate::utils::EMPTY_TYPE_ID; +use crate::utils::{StaticVec, EMPTY_TYPE_ID}; #[cfg(not(feature = "no_module"))] use crate::module::ModuleRef; @@ -195,7 +195,7 @@ pub struct FnDef { /// Function access mode. pub access: FnAccess, /// Names of function parameters. - pub params: Vec, + pub params: StaticVec, /// Function body. pub body: Stmt, /// Position of the function definition. @@ -294,7 +294,7 @@ pub enum Stmt { /// const id = expr Const(Box<((String, Position), Expr)>), /// { stmt; ... } - Block(Box<(Vec, Position)>), + Block(Box<(StaticVec, Position)>), /// { stmt } Expr(Box), /// continue @@ -306,7 +306,13 @@ pub enum Stmt { /// import expr as module Import(Box<(Expr, (String, Position))>), /// expr id as name, ... - Export(Box)>>), + Export(Box)>>), +} + +impl Default for Stmt { + fn default() -> Self { + Self::Noop(Default::default()) + } } impl Stmt { @@ -324,7 +330,7 @@ impl Stmt { Stmt::Loop(x) => x.position(), Stmt::For(x) => x.2.position(), Stmt::Import(x) => (x.1).1, - Stmt::Export(x) => (x.get(0).unwrap().0).1, + Stmt::Export(x) => (x.get(0).0).1, } } @@ -406,7 +412,7 @@ pub enum Expr { (Cow<'static, str>, Position), MRef, u64, - Vec, + StaticVec, Option, )>, ), @@ -417,9 +423,9 @@ pub enum Expr { /// expr[expr] Index(Box<(Expr, Expr, Position)>), /// [ expr, ... ] - Array(Box<(Vec, Position)>), + Array(Box<(StaticVec, Position)>), /// #{ name:expr, ... } - Map(Box<(Vec<((String, Position), Expr)>, Position)>), + Map(Box<(StaticVec<((String, Position), Expr)>, Position)>), /// lhs in rhs In(Box<(Expr, Expr, Position)>), /// lhs && rhs @@ -434,6 +440,12 @@ pub enum Expr { Unit(Position), } +impl Default for Expr { + fn default() -> Self { + Self::Unit(Default::default()) + } +} + impl Expr { /// Get the `Dynamic` value of a constant expression. /// @@ -713,7 +725,7 @@ fn parse_call_expr<'a>( begin: Position, allow_stmt_expr: bool, ) -> Result> { - let mut args = Vec::new(); + let mut args = StaticVec::new(); match input.peek().unwrap() { // id @@ -733,7 +745,7 @@ fn parse_call_expr<'a>( #[cfg(not(feature = "no_module"))] let hash_fn_def = { if let Some(modules) = modules.as_mut() { - modules.set_index(stack.find_module(&modules.get_ref(0).0)); + modules.set_index(stack.find_module(&modules.get(0).0)); // Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, @@ -774,7 +786,7 @@ fn parse_call_expr<'a>( #[cfg(not(feature = "no_module"))] let hash_fn_def = { if let Some(modules) = modules.as_mut() { - modules.set_index(stack.find_module(&modules.get_ref(0).0)); + modules.set_index(stack.find_module(&modules.get(0).0)); // Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, @@ -1013,7 +1025,7 @@ fn parse_array_literal<'a>( pos: Position, allow_stmt_expr: bool, ) -> Result> { - let mut arr = Vec::new(); + let mut arr = StaticVec::new(); if !match_token(input, Token::RightBracket)? { while !input.peek().unwrap().0.is_eof() { @@ -1056,7 +1068,7 @@ fn parse_map_literal<'a>( pos: Position, allow_stmt_expr: bool, ) -> Result> { - let mut map = Vec::new(); + let mut map = StaticVec::new(); if !match_token(input, Token::RightBrace)? { while !input.peek().unwrap().0.is_eof() { @@ -1239,7 +1251,7 @@ fn parse_primary<'a>( // Qualifiers + variable name *hash = calc_fn_hash(modules.iter().map(|(v, _)| v.as_str()), name, empty()); - modules.set_index(stack.find_module(&modules.get_ref(0).0)); + modules.set_index(stack.find_module(&modules.get(0).0)); } _ => (), } @@ -1296,15 +1308,17 @@ fn parse_unary<'a>( Expr::FloatConstant(x) => Ok(Expr::FloatConstant(Box::new((-x.0, x.1)))), // Call negative function - e => { + expr => { let op = "-"; let hash = calc_fn_hash(empty(), op, repeat(EMPTY_TYPE_ID()).take(2)); + let mut args = StaticVec::new(); + args.push(expr); Ok(Expr::FnCall(Box::new(( (op.into(), pos), None, hash, - vec![e], + args, None, )))) } @@ -1318,7 +1332,8 @@ fn parse_unary<'a>( // !expr (Token::Bang, _) => { let pos = eat_token(input, Token::Bang); - let expr = vec![parse_primary(input, stack, allow_stmt_expr)?]; + let mut args = StaticVec::new(); + args.push(parse_primary(input, stack, allow_stmt_expr)?); let op = "!"; let hash = calc_fn_hash(empty(), op, repeat(EMPTY_TYPE_ID()).take(2)); @@ -1327,7 +1342,7 @@ fn parse_unary<'a>( (op.into(), pos), None, hash, - expr, + args, Some(false.into()), // NOT operator, when operating on invalid operand, defaults to false )))) } @@ -1412,9 +1427,13 @@ fn parse_op_assignment_stmt<'a>( let rhs = parse_expr(input, stack, allow_stmt_expr)?; // lhs op= rhs -> lhs = op(lhs, rhs) - let args = vec![lhs_copy, rhs]; + let mut args = StaticVec::new(); + args.push(lhs_copy); + args.push(rhs); + let hash = calc_fn_hash(empty(), op, repeat(EMPTY_TYPE_ID()).take(args.len())); let rhs_expr = Expr::FnCall(Box::new(((op.into(), pos), None, hash, args, None))); + make_assignment_stmt(stack, lhs, rhs_expr, pos) } @@ -1452,7 +1471,7 @@ fn make_dot_expr( #[cfg(feature = "no_module")] unreachable!(); #[cfg(not(feature = "no_module"))] - return Err(PERR::PropertyExpected.into_err(x.1.unwrap().get_ref(0).1)); + return Err(PERR::PropertyExpected.into_err(x.1.unwrap().get(0).1)); } // lhs.dot_lhs.dot_rhs (lhs, Expr::Dot(x)) => { @@ -1695,7 +1714,10 @@ fn parse_binary_op<'a>( let cmp_def = Some(false.into()); let op = op_token.syntax(); let hash = calc_fn_hash(empty(), &op, repeat(EMPTY_TYPE_ID()).take(2)); - let mut args = vec![current_lhs, rhs]; + + let mut args = StaticVec::new(); + args.push(current_lhs); + args.push(rhs); current_lhs = match op_token { Token::Plus => Expr::FnCall(Box::new(((op, pos), None, hash, args, None))), @@ -1721,13 +1743,13 @@ fn parse_binary_op<'a>( } Token::Or => { - let rhs = args.pop().unwrap(); - let current_lhs = args.pop().unwrap(); + let rhs = args.pop(); + let current_lhs = args.pop(); Expr::Or(Box::new((current_lhs, rhs, pos))) } Token::And => { - let rhs = args.pop().unwrap(); - let current_lhs = args.pop().unwrap(); + let rhs = args.pop(); + let current_lhs = args.pop(); Expr::And(Box::new((current_lhs, rhs, pos))) } Token::Ampersand => Expr::FnCall(Box::new(((op, pos), None, hash, args, None))), @@ -1735,15 +1757,15 @@ fn parse_binary_op<'a>( Token::XOr => Expr::FnCall(Box::new(((op, pos), None, hash, args, None))), Token::In => { - let rhs = args.pop().unwrap(); - let current_lhs = args.pop().unwrap(); + let rhs = args.pop(); + let current_lhs = args.pop(); make_in_expr(current_lhs, rhs, pos)? } #[cfg(not(feature = "no_object"))] Token::Period => { - let mut rhs = args.pop().unwrap(); - let current_lhs = args.pop().unwrap(); + let mut rhs = args.pop(); + let current_lhs = args.pop(); match &mut rhs { // current_lhs.rhs(...) - method call @@ -2025,7 +2047,7 @@ fn parse_import<'a>( fn parse_export<'a>(input: &mut Peekable>) -> Result> { eat_token(input, Token::Export); - let mut exports = Vec::new(); + let mut exports = StaticVec::new(); loop { let (id, id_pos) = match input.next().unwrap() { @@ -2098,7 +2120,7 @@ fn parse_block<'a>( } }; - let mut statements = Vec::new(); + let mut statements = StaticVec::new(); let prev_len = stack.len(); while !match_token(input, Token::RightBrace)? { diff --git a/src/result.rs b/src/result.rs index c33c810b..4ae0d9e5 100644 --- a/src/result.rs +++ b/src/result.rs @@ -79,8 +79,14 @@ pub enum EvalAltResult { ErrorDotExpr(String, Position), /// Arithmetic error encountered. Wrapped value is the error message. ErrorArithmetic(String, Position), + /// Number of operations over maximum limit. + ErrorTooManyOperations(Position), + /// Modules over maximum limit. + ErrorTooManyModules(Position), /// Call stack over maximum limit. ErrorStackOverflow(Position), + /// The script is prematurely terminated. + ErrorTerminated(Position), /// Run-time error encountered. Wrapped value is the error message. ErrorRuntime(String, Position), @@ -137,7 +143,10 @@ impl EvalAltResult { Self::ErrorInExpr(_) => "Malformed 'in' expression", Self::ErrorDotExpr(_, _) => "Malformed dot expression", Self::ErrorArithmetic(_, _) => "Arithmetic error", + Self::ErrorTooManyOperations(_) => "Too many operations", + Self::ErrorTooManyModules(_) => "Too many modules imported", Self::ErrorStackOverflow(_) => "Stack overflow", + Self::ErrorTerminated(_) => "Script terminated.", Self::ErrorRuntime(_, _) => "Runtime error", Self::ErrorLoopBreak(true, _) => "Break statement not inside a loop", Self::ErrorLoopBreak(false, _) => "Continue statement not inside a loop", @@ -183,7 +192,10 @@ impl fmt::Display for EvalAltResult { | Self::ErrorAssignmentToUnknownLHS(pos) | Self::ErrorInExpr(pos) | Self::ErrorDotExpr(_, pos) - | Self::ErrorStackOverflow(pos) => write!(f, "{} ({})", desc, pos), + | Self::ErrorTooManyOperations(pos) + | Self::ErrorTooManyModules(pos) + | Self::ErrorStackOverflow(pos) + | Self::ErrorTerminated(pos) => write!(f, "{} ({})", desc, pos), Self::ErrorRuntime(s, pos) => { write!(f, "{} ({})", if s.is_empty() { desc } else { s }, pos) @@ -299,7 +311,10 @@ impl EvalAltResult { | Self::ErrorInExpr(pos) | Self::ErrorDotExpr(_, pos) | Self::ErrorArithmetic(_, pos) + | Self::ErrorTooManyOperations(pos) + | Self::ErrorTooManyModules(pos) | Self::ErrorStackOverflow(pos) + | Self::ErrorTerminated(pos) | Self::ErrorRuntime(_, pos) | Self::ErrorLoopBreak(_, pos) | Self::Return(_, pos) => *pos, @@ -335,7 +350,10 @@ impl EvalAltResult { | Self::ErrorInExpr(pos) | Self::ErrorDotExpr(_, pos) | Self::ErrorArithmetic(_, pos) + | Self::ErrorTooManyOperations(pos) + | Self::ErrorTooManyModules(pos) | Self::ErrorStackOverflow(pos) + | Self::ErrorTerminated(pos) | Self::ErrorRuntime(_, pos) | Self::ErrorLoopBreak(_, pos) | Self::Return(_, pos) => *pos = new_position, diff --git a/src/scope.rs b/src/scope.rs index 17909970..1b9c8b1d 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -36,7 +36,7 @@ pub struct Entry<'a> { pub expr: Option>, } -/// A type containing information about the current scope. +/// Type containing information about the current scope. /// Useful for keeping state between `Engine` evaluation runs. /// /// # Example diff --git a/src/token.rs b/src/token.rs index 25d26ff7..97df2d54 100644 --- a/src/token.rs +++ b/src/token.rs @@ -2,6 +2,7 @@ use crate::error::LexError; use crate::parser::INT; +use crate::utils::StaticVec; #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; @@ -425,7 +426,7 @@ pub struct TokenIterator<'a> { /// Current position. pos: Position, /// The input character streams. - streams: Vec>>, + streams: StaticVec>>, } impl<'a> TokenIterator<'a> { diff --git a/src/unsafe.rs b/src/unsafe.rs index 47d5b40c..46d91198 100644 --- a/src/unsafe.rs +++ b/src/unsafe.rs @@ -2,7 +2,6 @@ use crate::any::Variant; use crate::engine::State; -use crate::utils::StaticVec; use crate::stdlib::{ any::{Any, TypeId}, @@ -10,7 +9,6 @@ use crate::stdlib::{ boxed::Box, mem, ptr, string::ToString, - vec::Vec, }; /// Cast a type into another type. @@ -49,20 +47,17 @@ pub fn unsafe_cast_box(item: Box) -> Result, B /// current `Scope` without cloning the variable name. Doing this is safe because all local /// variables in the `Scope` are cleared out before existing the block. /// -/// Force-casting a local variable lifetime to the current `Scope`'s larger lifetime saves +/// Force-casting a local variable's lifetime to the current `Scope`'s larger lifetime saves /// on allocations and string cloning, thus avoids us having to maintain a chain of `Scope`'s. -pub fn unsafe_cast_var_name<'s>(name: &str, state: &State) -> Cow<'s, str> { +pub fn unsafe_cast_var_name_to_lifetime<'s>(name: &str, state: &State) -> Cow<'s, str> { // If not at global level, we can force-cast if state.scope_level > 0 { // WARNING - force-cast the variable name into the scope's lifetime to avoid cloning it // this is safe because all local variables are cleared at the end of the block unsafe { mem::transmute::<_, &'s str>(name) }.into() } else { + // The variable is introduced at global (top) level and may persist after the script run. + // Therefore, clone the variable name. name.to_string().into() } } - -/// Provide a type instance that is memory-zeroed. -pub fn unsafe_zeroed() -> T { - unsafe { mem::MaybeUninit::zeroed().assume_init() } -} diff --git a/src/utils.rs b/src/utils.rs index f61d24ea..a8f73390 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,8 @@ //! Module containing various utility types and functions. - -use crate::r#unsafe::unsafe_zeroed; +//! +//! # Safety +//! +//! The `StaticVec` type has some `unsafe` blocks to handle conversions between `MaybeUninit` and regular types. use crate::stdlib::{ any::TypeId, @@ -8,6 +10,8 @@ use crate::stdlib::{ hash::{Hash, Hasher}, iter::FromIterator, mem, + mem::MaybeUninit, + ops::{Drop, Index, IndexMut}, vec::Vec, }; @@ -47,29 +51,107 @@ pub fn calc_fn_spec<'a>( s.finish() } -/// A type to hold a number of values in static storage for speed, and any spill-overs in a `Vec`. +/// A type to hold a number of values in static storage for no-allocation, quick access. +/// If too many items are stored, it converts into using a `Vec`. /// /// This is essentially a knock-off of the [`staticvec`](https://crates.io/crates/staticvec) crate. /// This simplified implementation here is to avoid pulling in another crate. /// +/// # Implementation +/// +/// A `StaticVec` holds data in _either one_ of two storages: 1) a fixed-size array of `MAX_STATIC_VEC` +/// items, and 2) a dynamic `Vec`. At any time, either one of them (or both) must be empty, depending on the +/// total number of items. +/// +/// There is a `len` field containing the total number of items held by the `StaticVec`. +/// +/// The fixed-size array (`list`) is not initialized (i.e. initialized with `MaybeUninit::uninit()`). +/// +/// When `len <= MAX_STATIC_VEC`, all elements are stored in the fixed-size array. +/// Array slots `>= len` are `MaybeUninit::uninit()` while slots `< len` are considered actual data. +/// In this scenario, the `Vec` (`more`) is empty. +/// +/// As soon as we try to push a new item into the `StaticVec` that makes the total number exceed +/// `MAX_STATIC_VEC`, all the items in the fixed-sized array are taken out, replaced with +/// `MaybeUninit::uninit()` (via `mem::replace`) and pushed into the `Vec`. +/// Then the new item is added to the `Vec`. +/// +/// Therefore, if `len > MAX_STATIC_VEC`, then the fixed-size array (`list`) is considered +/// empty and uninitialized while all data resides in the `Vec` (`more`). +/// +/// When popping an item off of the `StaticVec`, the reverse is true. When `len = MAX_STATIC_VEC + 1`, +/// after popping the item, all the items residing in the `Vec` are moved back to the fixed-size array (`list`). +/// The `Vec` will then be empty. +/// +/// Therefore, if `len <= MAX_STATIC_VEC`, data is in the fixed-size array (`list`). +/// Otherwise, data is in the `Vec` (`more`). +/// /// # Safety /// -/// This type uses some unsafe code (mainly to zero out unused array slots) for efficiency. +/// This type uses some unsafe code (mainly for uninitialized/unused array slots) for efficiency. // // TODO - remove unsafe code -#[derive(Clone, Hash)] pub struct StaticVec { /// Total number of values held. len: usize, - /// Static storage. 4 slots should be enough for most cases - i.e. four levels of indirection. - list: [T; 4], + /// Fixed-size storage for fast, no-allocation access. + list: [MaybeUninit; MAX_STATIC_VEC], /// Dynamic storage. For spill-overs. more: Vec, } +/// Maximum slots of fixed-size storage for a `StaticVec`. +/// 4 slots should be enough for most cases. +const MAX_STATIC_VEC: usize = 4; + +impl Drop for StaticVec { + fn drop(&mut self) { + self.clear(); + } +} + +impl Default for StaticVec { + fn default() -> Self { + Self { + len: 0, + list: unsafe { mem::MaybeUninit::uninit().assume_init() }, + more: Vec::new(), + } + } +} + impl PartialEq for StaticVec { fn eq(&self, other: &Self) -> bool { - self.len == other.len && self.list == other.list && self.more == other.more + if self.len != other.len || self.more != other.more { + return false; + } + + if self.len > MAX_STATIC_VEC { + return true; + } + + unsafe { + mem::transmute::<_, &[T; MAX_STATIC_VEC]>(&self.list) + == mem::transmute::<_, &[T; MAX_STATIC_VEC]>(&other.list) + } + } +} + +impl Clone for StaticVec { + fn clone(&self) -> Self { + let mut value: Self = Default::default(); + value.len = self.len; + + if self.is_fixed_storage() { + for x in 0..self.len { + let item: &T = unsafe { mem::transmute(self.list.get(x).unwrap()) }; + value.list[x] = MaybeUninit::new(item.clone()); + } + } else { + value.more = self.more.clone(); + } + + value } } @@ -87,35 +169,121 @@ impl FromIterator for StaticVec { } } -impl Default for StaticVec { - fn default() -> Self { - Self { - len: 0, - list: unsafe_zeroed(), - more: Vec::new(), - } - } -} - impl StaticVec { /// Create a new `StaticVec`. pub fn new() -> Self { Default::default() } + /// Empty the `StaticVec`. + pub fn clear(&mut self) { + if self.is_fixed_storage() { + for x in 0..self.len { + self.extract_from_list(x); + } + } else { + self.more.clear(); + } + self.len = 0; + } + /// Extract a `MaybeUninit` into a concrete initialized type. + fn extract(value: MaybeUninit) -> T { + unsafe { value.assume_init() } + } + /// Extract an item from the fixed-size array, replacing it with `MaybeUninit::uninit()`. + /// + /// # Panics + /// + /// Panics if fixed-size storage is not used, or if the `index` is out of bounds. + fn extract_from_list(&mut self, index: usize) -> T { + if !self.is_fixed_storage() { + panic!("not fixed storage in StaticVec"); + } + if index >= self.len { + panic!("index OOB in StaticVec"); + } + Self::extract(mem::replace( + self.list.get_mut(index).unwrap(), + MaybeUninit::uninit(), + )) + } + /// Set an item into the fixed-size array. + /// If `drop` is `true`, the original value is extracted then automatically dropped. + /// + /// # Panics + /// + /// Panics if fixed-size storage is not used, or if the `index` is out of bounds. + fn set_into_list(&mut self, index: usize, value: T, drop: bool) { + if !self.is_fixed_storage() { + panic!("not fixed storage in StaticVec"); + } + // Allow setting at most one slot to the right + if index > self.len { + panic!("index OOB in StaticVec"); + } + let temp = mem::replace(self.list.get_mut(index).unwrap(), MaybeUninit::new(value)); + if drop { + // Extract the original value - which will drop it automatically + Self::extract(temp); + } + } + /// Move item in the fixed-size array into the `Vec`. + /// + /// # Panics + /// + /// Panics if fixed-size storage is not used, or if the fixed-size storage is not full. + fn move_fixed_into_vec(&mut self, num: usize) { + if !self.is_fixed_storage() { + panic!("not fixed storage in StaticVec"); + } + if self.len != num { + panic!("fixed storage is not full in StaticVec"); + } + self.more.extend( + self.list + .iter_mut() + .take(num) + .map(|v| mem::replace(v, MaybeUninit::uninit())) + .map(Self::extract), + ); + } + /// Is data stored in fixed-size storage? + fn is_fixed_storage(&self) -> bool { + self.len <= MAX_STATIC_VEC + } /// Push a new value to the end of this `StaticVec`. pub fn push>(&mut self, value: X) { - if self.len == self.list.len() { - // Move the fixed list to the Vec - for x in 0..self.list.len() { - let def_val: T = unsafe_zeroed(); - self.more - .push(mem::replace(self.list.get_mut(x).unwrap(), def_val)); - } - self.more.push(value.into()); - } else if self.len > self.list.len() { + if self.len == MAX_STATIC_VEC { + self.move_fixed_into_vec(MAX_STATIC_VEC); self.more.push(value.into()); + } else if self.is_fixed_storage() { + self.set_into_list(self.len, value.into(), false); } else { - self.list[self.len] = value.into(); + self.more.push(value.into()); + } + self.len += 1; + } + /// Insert a new value to this `StaticVec` at a particular position. + /// + /// # Panics + /// + /// Panics if `index` is out of bounds. + pub fn insert>(&mut self, index: usize, value: X) { + if index > self.len { + panic!("index OOB in StaticVec"); + } + + if self.len == MAX_STATIC_VEC { + self.move_fixed_into_vec(MAX_STATIC_VEC); + self.more.insert(index, value.into()); + } else if self.is_fixed_storage() { + // Move all items one slot to the right + for x in (index..self.len).rev() { + let orig_value = self.extract_from_list(x); + self.set_into_list(x + 1, orig_value, false); + } + self.set_into_list(index, value.into(), false); + } else { + self.more.insert(index, value.into()); } self.len += 1; } @@ -125,22 +293,62 @@ impl StaticVec { /// /// Panics if the `StaticVec` is empty. pub fn pop(&mut self) -> T { - let result = if self.len <= 0 { - panic!("nothing to pop!") - } else if self.len <= self.list.len() { - let def_val: T = unsafe_zeroed(); - mem::replace(self.list.get_mut(self.len - 1).unwrap(), def_val) + if self.is_empty() { + panic!("nothing to pop!"); + } + + let result = if self.is_fixed_storage() { + self.extract_from_list(self.len - 1) } else { - let r = self.more.pop().unwrap(); + let value = self.more.pop().unwrap(); // Move back to the fixed list - if self.more.len() == self.list.len() { - for x in 0..self.list.len() { - self.list[self.list.len() - 1 - x] = self.more.pop().unwrap(); + if self.more.len() == MAX_STATIC_VEC { + for index in (0..MAX_STATIC_VEC).rev() { + let item = self.more.pop().unwrap(); + self.set_into_list(index, item, false); } } - r + value + }; + + self.len -= 1; + + result + } + /// Remove a value from this `StaticVec` at a particular position. + /// + /// # Panics + /// + /// Panics if `index` is out of bounds. + pub fn remove(&mut self, index: usize) -> T { + if index >= self.len { + panic!("index OOB in StaticVec"); + } + + let result = if self.is_fixed_storage() { + let value = self.extract_from_list(index); + + // Move all items one slot to the left + for x in index..self.len - 1 { + let orig_value = self.extract_from_list(x + 1); + self.set_into_list(x, orig_value, false); + } + + value + } else { + let value = self.more.remove(index); + + // Move back to the fixed list + if self.more.len() == MAX_STATIC_VEC { + for index in (0..MAX_STATIC_VEC).rev() { + let item = self.more.pop().unwrap(); + self.set_into_list(index, item, false); + } + } + + value }; self.len -= 1; @@ -151,18 +359,24 @@ impl StaticVec { pub fn len(&self) -> usize { self.len } + /// Is this `StaticVec` empty? + pub fn is_empty(&self) -> bool { + self.len == 0 + } /// Get a reference to the item at a particular index. /// /// # Panics /// - /// Panics if the index is out of bounds. - pub fn get_ref(&self, index: usize) -> &T { + /// Panics if `index` is out of bounds. + pub fn get(&self, index: usize) -> &T { if index >= self.len { panic!("index OOB in StaticVec"); } - if self.len < self.list.len() { - self.list.get(index).unwrap() + let list: &[T; MAX_STATIC_VEC] = unsafe { mem::transmute(&self.list) }; + + if self.is_fixed_storage() { + list.get(index).unwrap() } else { self.more.get(index).unwrap() } @@ -171,52 +385,106 @@ impl StaticVec { /// /// # Panics /// - /// Panics if the index is out of bounds. + /// Panics if `index` is out of bounds. pub fn get_mut(&mut self, index: usize) -> &mut T { if index >= self.len { panic!("index OOB in StaticVec"); } - if self.len < self.list.len() { - self.list.get_mut(index).unwrap() + let list: &mut [T; MAX_STATIC_VEC] = unsafe { mem::transmute(&mut self.list) }; + + if self.is_fixed_storage() { + list.get_mut(index).unwrap() } else { self.more.get_mut(index).unwrap() } } /// Get an iterator to entries in the `StaticVec`. pub fn iter(&self) -> impl Iterator { - if self.len > self.list.len() { - self.more.iter() + let list: &[T; MAX_STATIC_VEC] = unsafe { mem::transmute(&self.list) }; + + if self.is_fixed_storage() { + list[..self.len].iter() } else { - self.list[..self.len].iter() + self.more.iter() } } /// Get a mutable iterator to entries in the `StaticVec`. pub fn iter_mut(&mut self) -> impl Iterator { - if self.len > self.list.len() { - self.more.iter_mut() + let list: &mut [T; MAX_STATIC_VEC] = unsafe { mem::transmute(&mut self.list) }; + + if self.is_fixed_storage() { + list[..self.len].iter_mut() } else { - self.list[..self.len].iter_mut() + self.more.iter_mut() } } } -impl StaticVec { - /// Get the item at a particular index. +impl StaticVec { + /// Get a mutable iterator to entries in the `StaticVec`. + pub fn into_iter(mut self) -> Box> { + if self.is_fixed_storage() { + let mut it = FixedStorageIterator { + data: unsafe { mem::MaybeUninit::uninit().assume_init() }, + index: 0, + limit: self.len, + }; + + for x in 0..self.len { + it.data[x] = mem::replace(self.list.get_mut(x).unwrap(), MaybeUninit::uninit()); + } + self.len = 0; + + Box::new(it) + } else { + Box::new(Vec::from(self).into_iter()) + } + } +} + +/// An iterator that takes control of the fixed-size storage of a `StaticVec` and returns its values. +struct FixedStorageIterator { + data: [MaybeUninit; MAX_STATIC_VEC], + index: usize, + limit: usize, +} + +impl Iterator for FixedStorageIterator { + type Item = T; + + fn next(&mut self) -> Option { + if self.index >= self.limit { + None + } else { + self.index += 1; + + let value = mem::replace( + self.data.get_mut(self.index - 1).unwrap(), + MaybeUninit::uninit(), + ); + + unsafe { Some(value.assume_init()) } + } + } +} + +impl StaticVec { + /// Get the item at a particular index, replacing it with the default. /// /// # Panics /// - /// Panics if the index is out of bounds. - pub fn get(&self, index: usize) -> T { + /// Panics if `index` is out of bounds. + pub fn take(&mut self, index: usize) -> T { if index >= self.len { panic!("index OOB in StaticVec"); } - if self.len < self.list.len() { - *self.list.get(index).unwrap() + mem::take(if self.is_fixed_storage() { + unsafe { mem::transmute(self.list.get_mut(index).unwrap()) } } else { - *self.more.get(index).unwrap() - } + self.more.get_mut(index).unwrap() + }) } } @@ -230,20 +498,68 @@ impl fmt::Debug for StaticVec { impl AsRef<[T]> for StaticVec { fn as_ref(&self) -> &[T] { - if self.len > self.list.len() { - &self.more[..] + let list: &[T; MAX_STATIC_VEC] = unsafe { mem::transmute(&self.list) }; + + if self.is_fixed_storage() { + &list[..self.len] } else { - &self.list[..self.len] + &self.more[..] } } } impl AsMut<[T]> for StaticVec { fn as_mut(&mut self) -> &mut [T] { - if self.len > self.list.len() { - &mut self.more[..] + let list: &mut [T; MAX_STATIC_VEC] = unsafe { mem::transmute(&mut self.list) }; + + if self.is_fixed_storage() { + &mut list[..self.len] } else { - &mut self.list[..self.len] + &mut self.more[..] } } } + +impl Index for StaticVec { + type Output = T; + + fn index(&self, index: usize) -> &Self::Output { + self.get(index) + } +} + +impl IndexMut for StaticVec { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + self.get_mut(index) + } +} + +impl From> for Vec { + fn from(mut value: StaticVec) -> Self { + if value.len <= MAX_STATIC_VEC { + value.move_fixed_into_vec(value.len); + } + value.len = 0; + + let mut arr = Self::new(); + arr.append(&mut value.more); + arr + } +} + +impl From> for StaticVec { + fn from(mut value: Vec) -> Self { + let mut arr: Self = Default::default(); + arr.len = value.len(); + + if arr.len <= MAX_STATIC_VEC { + for x in (0..arr.len).rev() { + arr.set_into_list(x, value.pop().unwrap(), false); + } + } else { + arr.more = value; + } + + arr + } +} diff --git a/tests/get_set.rs b/tests/get_set.rs index 57712304..94c3064a 100644 --- a/tests/get_set.rs +++ b/tests/get_set.rs @@ -43,20 +43,20 @@ fn test_get_set() -> Result<(), Box> { engine.register_fn("new_ts", TestStruct::new); #[cfg(not(feature = "no_index"))] - engine.register_indexer(|value: &mut TestStruct, index: INT| value.array[index as usize]); + engine.register_indexer(|value: &mut TestStruct, index: String| value.array[index.len()]); assert_eq!(engine.eval::("let a = new_ts(); a.x = 500; a.x")?, 500); assert_eq!(engine.eval::("let a = new_ts(); a.x.add(); a.x")?, 42); assert_eq!(engine.eval::("let a = new_ts(); a.y.add(); a.y")?, 0); #[cfg(not(feature = "no_index"))] - assert_eq!(engine.eval::("let a = new_ts(); a[3]")?, 4); + assert_eq!(engine.eval::(r#"let a = new_ts(); a["abc"]"#)?, 4); Ok(()) } #[test] -fn test_big_get_set() -> Result<(), Box> { +fn test_get_set_chain() -> Result<(), Box> { #[derive(Clone)] struct TestChild { x: INT, diff --git a/tests/modules.rs b/tests/modules.rs index acf3c0e6..9c6c8ec4 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -72,13 +72,72 @@ fn test_module_resolver() -> Result<(), Box> { assert_eq!( engine.eval::( r#" - import "hello" as h; - h::answer + import "hello" as h1; + import "hello" as h2; + h2::answer "# )?, 42 ); + engine.set_max_modules(5); + + assert!(matches!( + *engine + .eval::( + r#" + let x = 0; + + for x in range(0, 10) { + import "hello" as h; + x += h::answer; + } + + x + "# + ) + .expect_err("should error"), + EvalAltResult::ErrorTooManyModules(_) + )); + + #[cfg(not(feature = "no_function"))] + assert!(matches!( + *engine + .eval::( + r#" + let x = 0; + + fn foo() { + import "hello" as h; + x += h::answer; + } + + for x in range(0, 10) { + foo(); + } + + x + "# + ) + .expect_err("should error"), + EvalAltResult::ErrorInFunctionCall(fn_name, _, _) if fn_name == "foo" + )); + + engine.set_max_modules(0); + + #[cfg(not(feature = "no_function"))] + engine.eval::<()>( + r#" + fn foo() { + import "hello" as h; + } + + for x in range(0, 10) { + foo(); + } + "#, + )?; + Ok(()) } diff --git a/tests/operations.rs b/tests/operations.rs new file mode 100644 index 00000000..c02e2381 --- /dev/null +++ b/tests/operations.rs @@ -0,0 +1,113 @@ +#![cfg(not(feature = "unchecked"))] +use rhai::{Engine, EvalAltResult}; + +#[test] +fn test_max_operations() -> Result<(), Box> { + 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> { + let mut engine = Engine::new(); + engine.set_max_operations(500); + + engine.on_progress(|count| { + if count % 100 == 0 { + println!("{}", count); + } + true + }); + + engine.eval::<()>( + r#" + print("Test1"); + let x = 0; + + while x < 28 { + print(x); + x += 1; + } + "#, + )?; + + #[cfg(not(feature = "no_function"))] + engine.eval::<()>( + r#" + print("Test2"); + fn inc(x) { x + 1 } + let x = 0; + while x < 20 { x = inc(x); } + "#, + )?; + + #[cfg(not(feature = "no_function"))] + assert!(matches!( + *engine + .eval::<()>( + r#" + print("Test3"); + fn inc(x) { x + 1 } + let x = 0; + + while x < 28 { + print(x); + x = inc(x); + } + "#, + ) + .expect_err("should error"), + EvalAltResult::ErrorTooManyOperations(_) + )); + + Ok(()) +} + +#[test] +fn test_max_operations_eval() -> Result<(), Box> { + 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(()) +} diff --git a/tests/stack.rs b/tests/stack.rs index 1eeb4250..5ebb550b 100644 --- a/tests/stack.rs +++ b/tests/stack.rs @@ -15,6 +15,7 @@ fn test_stack_overflow() -> Result<(), Box> { 325 ); + #[cfg(not(feature = "unchecked"))] match engine.eval::<()>( r" fn foo(n) { if n == 0 { 0 } else { n + foo(n-1) } }