Merge pull request #154 from schungx/master

Limiting script resources
This commit is contained in:
Stephen Chung 2020-05-18 10:08:49 +08:00 committed by GitHub
commit e5584b2471
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1279 additions and 346 deletions

196
README.md
View File

@ -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`.
@ -64,22 +69,23 @@ 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. |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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, [`Engine`], [`Scope`] and `AST` are all `Send + Sync`. |
| `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.<br/>The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.<br/>`FileModuleResolver::create_module()` loads a script file and returns a module. |
| `StaticModuleResolver` | Loads modules that are statically added. This can be used when the [`no_std`] feature is turned on. |
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<EvalAltResult>> {
Err(format!("eval is evil! I refuse to run {}", script).into())
}

66
RELEASES.md Normal file
View File

@ -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.

View File

@ -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<T: Any + Clone> 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<T>` (into `Array`) and `HashMap<String, T>` (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.

View File

@ -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<T, Box<EvalAltResult>> {
// 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<T, Box<EvalAltResult>> {
let result = self.eval_ast_with_scope_raw(scope, ast)?;
let (result, _) = self.eval_ast_with_scope_raw(scope, ast)?;
let return_type = self.map_type_name(result.type_name());
@ -881,7 +882,7 @@ impl Engine {
&self,
scope: &mut Scope,
ast: &AST,
) -> Result<Dynamic, Box<EvalAltResult>> {
) -> Result<(Dynamic, u64), Box<EvalAltResult>> {
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<rhai::EvalAltResult>> {
/// # use std::sync::RwLock;
/// # use std::sync::Arc;
/// use rhai::Engine;
///
/// let result = Arc::new(RwLock::new(0_u64));
/// let logger = result.clone();
///
/// let mut engine = Engine::new();
///
/// engine.on_progress(move |ops| {
/// if ops > 10000 {
/// false
/// } else if ops % 800 == 0 {
/// *logger.write().unwrap() = ops;
/// true
/// } else {
/// true
/// }
/// });
///
/// engine.consume("for x in range(0, 50000) {}")
/// .expect_err("should error");
///
/// assert_eq!(*result.read().unwrap(), 9600);
///
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "sync")]
pub fn on_progress(&mut self, callback: impl Fn(u64) -> bool + Send + Sync + 'static) {
self.progress = Some(Box::new(callback));
}
/// Register a callback for script evaluation progress.
///
/// # Example
///
/// ```
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
/// # use std::cell::Cell;
/// # use std::rc::Rc;
/// use rhai::Engine;
///
/// let result = Rc::new(Cell::new(0_u64));
/// let logger = result.clone();
///
/// let mut engine = Engine::new();
///
/// engine.on_progress(move |ops| {
/// if ops > 10000 {
/// false
/// } else if ops % 800 == 0 {
/// logger.set(ops);
/// true
/// } else {
/// true
/// }
/// });
///
/// engine.consume("for x in range(0, 50000) {}")
/// .expect_err("should error");
///
/// assert_eq!(result.get(), 9600);
///
/// # Ok(())
/// # }
/// ```
#[cfg(not(feature = "sync"))]
pub fn on_progress(&mut self, callback: impl Fn(u64) -> bool + 'static) {
self.progress = Some(Box::new(callback));
}
/// Override default action of `print` (print to stdout using `println!`)
///
/// # Example
@ -1092,21 +1172,21 @@ impl Engine {
///
/// ```
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
/// # 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<rhai::EvalAltResult>> {
/// # 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(())
/// # }
/// ```

View File

@ -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<Dynamic>;
/// 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<String, Dynamic>;
#[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<Dynamic>),
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)) => {
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<char> = s.chars().collect();
let ch = *chars.get_ref(x.1);
let ch = chars[*index];
// See if changed - if so, update the String
if ch != new_ch {
*chars.get_mut(x.1) = new_ch;
chars[*index] = new_ch;
s.clear();
chars.iter().for_each(|&ch| s.push(ch));
}
}
_ => unreachable!(),
},
}
Ok(())
@ -123,7 +142,7 @@ impl<'a> From<&'a mut Dynamic> for Target<'a> {
}
impl<T: Into<Dynamic>> From<T> 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<PrintCallback>,
/// Closure for implementing the `debug` command.
pub(crate) debug: Box<PrintCallback>,
/// Closure for progress reporting.
pub(crate) progress: Option<Box<ProgressCallback>>,
/// 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<NonZeroU64>,
/// Maximum number of modules allowed to load.
pub(crate) max_modules: Option<NonZeroU64>,
}
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::<Module>()
.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<EvalAltResult>> {
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<Dynamic, Box<EvalAltResult>> {
) -> Result<(Dynamic, State<'s>), Box<EvalAltResult>> {
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<Dynamic, Box<EvalAltResult>> {
@ -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<Dynamic>,
is_index: bool,
@ -870,12 +935,10 @@ impl Engine {
level: usize,
mut new_val: Option<Dynamic>,
) -> Result<(Dynamic, bool), Box<EvalAltResult>> {
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::<Map>() => {
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<EvalAltResult>> {
self.inc_operations(state, expr.position())?;
match expr {
Expr::FnCall(x) if x.1.is_none() => {
let mut arg_values = StaticVec::<Dynamic>::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::<Result<StaticVec<Dynamic>, _>>()?;
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<Target<'a>, Box<EvalAltResult>> {
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<Dynamic, Box<EvalAltResult>> {
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<Dynamic, Box<EvalAltResult>> {
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::<String>() {
if name == KEYWORD_EVAL && args.len() == 1 && args.get(0).is::<String>() {
let hash_fn = calc_fn_hash(empty(), name, once(TypeId::of::<String>()));
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<Dynamic, Box<EvalAltResult>> {
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)?
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(x.0.position())))
.map_err(|_| Box::new(EvalAltResult::ErrorLogicGuard(expr.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, 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::<String>()
{
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<EvalAltResult>> {
state.operations += 1;
#[cfg(not(feature = "unchecked"))]
{
// Guard against too many operations
if let Some(max) = self.max_operations {
if state.operations > max.get() {
return Err(Box::new(EvalAltResult::ErrorTooManyOperations(pos)));
}
}
}
// Report progress - only in steps
if let Some(progress) = &self.progress {
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

View File

@ -9,7 +9,7 @@ use crate::utils::StaticVec;
/// Any data type that can be converted into a `Vec<Dynamic>` can be used
/// as arguments to a function call.
pub trait FuncArgs {
/// Convert to a `Vec<Dynamic>` of the function call arguments.
/// Convert to a `StaticVec<Dynamic>` of the function call arguments.
fn into_vec(self) -> StaticVec<Dynamic>;
}

View File

@ -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<ARGS, RET> {
type Output;

View File

@ -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<T, U>: 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<Dynamic, Box<EvalAltResult>>;
}
/// 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.

View File

@ -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<FN, ARGS, RET> {
/// Register a custom function with the `Engine`.
///
@ -42,7 +42,7 @@ pub trait RegisterFn<FN, ARGS, RET> {
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<FN, ARGS> {
/// Register a custom function returning `Dynamic` values with the `Engine`.
///
@ -69,7 +69,7 @@ pub trait RegisterDynamicFn<FN, ARGS> {
fn register_dynamic_fn(&mut self, name: &str, f: FN);
}
/// A trait to register fallible custom functions returning `Result<_, Box<EvalAltResult>>` with the `Engine`.
/// Trait to register fallible custom functions returning `Result<_, Box<EvalAltResult>>` with the `Engine`.
pub trait RegisterResultFn<FN, ARGS, RET> {
/// Register a custom fallible function with the `Engine`.
///

View File

@ -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::*;

View File

@ -51,7 +51,7 @@ pub struct Module {
all_variables: HashMap<u64, Dynamic>,
/// External Rust functions.
functions: HashMap<u64, (String, FnAccess, Vec<TypeId>, SharedNativeFunction)>,
functions: HashMap<u64, (String, FnAccess, StaticVec<TypeId>, SharedNativeFunction)>,
/// Flattened collection of all external Rust functions, including those in sub-modules.
all_functions: HashMap<u64, SharedNativeFunction>,
@ -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<NonZeroUsize>);
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<Module, Box<EvalAltResult>>;
}
/// 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
///

View File

@ -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))
.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,

View File

@ -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<Module>;
#[derive(Clone, Default)]
pub(crate) struct PackagesCollection {
/// Collection of `PackageLibrary` instances.
packages: Vec<PackageLibrary>,
packages: StaticVec<PackageLibrary>,
}
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

View File

@ -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<String> {
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<String> {
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();
}

View File

@ -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<String>,
pub params: StaticVec<String>,
/// 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<Stmt>, Position)>),
Block(Box<(StaticVec<Stmt>, Position)>),
/// { stmt }
Expr(Box<Expr>),
/// continue
@ -306,7 +306,13 @@ pub enum Stmt {
/// import expr as module
Import(Box<(Expr, (String, Position))>),
/// expr id as name, ...
Export(Box<Vec<((String, Position), Option<(String, Position)>)>>),
Export(Box<StaticVec<((String, Position), Option<(String, Position)>)>>),
}
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<Expr>,
StaticVec<Expr>,
Option<Dynamic>,
)>,
),
@ -417,9 +423,9 @@ pub enum Expr {
/// expr[expr]
Index(Box<(Expr, Expr, Position)>),
/// [ expr, ... ]
Array(Box<(Vec<Expr>, Position)>),
Array(Box<(StaticVec<Expr>, 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<Expr, Box<ParseError>> {
let mut args = Vec::new();
let mut args = StaticVec::new();
match input.peek().unwrap() {
// id <EOF>
@ -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<Expr, Box<ParseError>> {
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<Expr, Box<ParseError>> {
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<TokenIterator<'a>>) -> Result<Stmt, Box<ParseError>> {
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)? {

View File

@ -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,

View File

@ -36,7 +36,7 @@ pub struct Entry<'a> {
pub expr: Option<Box<Expr>>,
}
/// A type containing information about the current scope.
/// Type containing information about the current scope.
/// Useful for keeping state between `Engine` evaluation runs.
///
/// # Example

View File

@ -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<Peekable<Chars<'a>>>,
streams: StaticVec<Peekable<Chars<'a>>>,
}
impl<'a> TokenIterator<'a> {

View File

@ -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<X: Variant, T: Variant>(item: Box<X>) -> Result<Box<T>, 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>() -> T {
unsafe { mem::MaybeUninit::zeroed().assume_init() }
}

View File

@ -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<T> {
/// 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<T>; MAX_STATIC_VEC],
/// Dynamic storage. For spill-overs.
more: Vec<T>,
}
/// Maximum slots of fixed-size storage for a `StaticVec`.
/// 4 slots should be enough for most cases.
const MAX_STATIC_VEC: usize = 4;
impl<T> Drop for StaticVec<T> {
fn drop(&mut self) {
self.clear();
}
}
impl<T> Default for StaticVec<T> {
fn default() -> Self {
Self {
len: 0,
list: unsafe { mem::MaybeUninit::uninit().assume_init() },
more: Vec::new(),
}
}
}
impl<T: PartialEq> PartialEq for StaticVec<T> {
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<T: Clone> Clone for StaticVec<T> {
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<T> FromIterator<T> for StaticVec<T> {
}
}
impl<T> Default for StaticVec<T> {
fn default() -> Self {
Self {
len: 0,
list: unsafe_zeroed(),
more: Vec::new(),
}
}
}
impl<T> StaticVec<T> {
/// 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>) -> 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<X: Into<T>>(&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<X: Into<T>>(&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<T> StaticVec<T> {
///
/// 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<T> StaticVec<T> {
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<T> StaticVec<T> {
///
/// # 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<Item = &T> {
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<Item = &mut T> {
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<T: Copy> StaticVec<T> {
/// Get the item at a particular index.
impl<T: 'static> StaticVec<T> {
/// Get a mutable iterator to entries in the `StaticVec`.
pub fn into_iter(mut self) -> Box<dyn Iterator<Item = T>> {
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<T> {
data: [MaybeUninit<T>; MAX_STATIC_VEC],
index: usize,
limit: usize,
}
impl<T> Iterator for FixedStorageIterator<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
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<T: Default> StaticVec<T> {
/// 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<T: fmt::Debug> fmt::Debug for StaticVec<T> {
impl<T> AsRef<[T]> for StaticVec<T> {
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<T> AsMut<[T]> for StaticVec<T> {
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<T> Index<usize> for StaticVec<T> {
type Output = T;
fn index(&self, index: usize) -> &Self::Output {
self.get(index)
}
}
impl<T> IndexMut<usize> for StaticVec<T> {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
self.get_mut(index)
}
}
impl<T> From<StaticVec<T>> for Vec<T> {
fn from(mut value: StaticVec<T>) -> 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<T> From<Vec<T>> for StaticVec<T> {
fn from(mut value: Vec<T>) -> 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
}
}

View File

@ -43,20 +43,20 @@ fn test_get_set() -> Result<(), Box<EvalAltResult>> {
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::<INT>("let a = new_ts(); a.x = 500; a.x")?, 500);
assert_eq!(engine.eval::<INT>("let a = new_ts(); a.x.add(); a.x")?, 42);
assert_eq!(engine.eval::<INT>("let a = new_ts(); a.y.add(); a.y")?, 0);
#[cfg(not(feature = "no_index"))]
assert_eq!(engine.eval::<INT>("let a = new_ts(); a[3]")?, 4);
assert_eq!(engine.eval::<INT>(r#"let a = new_ts(); a["abc"]"#)?, 4);
Ok(())
}
#[test]
fn test_big_get_set() -> Result<(), Box<EvalAltResult>> {
fn test_get_set_chain() -> Result<(), Box<EvalAltResult>> {
#[derive(Clone)]
struct TestChild {
x: INT,

View File

@ -72,13 +72,72 @@ fn test_module_resolver() -> Result<(), Box<EvalAltResult>> {
assert_eq!(
engine.eval::<INT>(
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::<INT>(
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::<INT>(
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(())
}

113
tests/operations.rs Normal file
View File

@ -0,0 +1,113 @@
#![cfg(not(feature = "unchecked"))]
use rhai::{Engine, EvalAltResult};
#[test]
fn test_max_operations() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
engine.set_max_operations(500);
engine.on_progress(|count| {
if count % 100 == 0 {
println!("{}", count);
}
true
});
engine.eval::<()>("let x = 0; while x < 20 { x += 1; }")?;
assert!(matches!(
*engine
.eval::<()>("for x in range(0, 500) {}")
.expect_err("should error"),
EvalAltResult::ErrorTooManyOperations(_)
));
engine.set_max_operations(0);
engine.eval::<()>("for x in range(0, 10000) {}")?;
Ok(())
}
#[test]
fn test_max_operations_functions() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
engine.set_max_operations(500);
engine.on_progress(|count| {
if count % 100 == 0 {
println!("{}", count);
}
true
});
engine.eval::<()>(
r#"
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<EvalAltResult>> {
let mut engine = Engine::new();
engine.set_max_operations(500);
engine.on_progress(|count| {
if count % 100 == 0 {
println!("{}", count);
}
true
});
assert!(matches!(
*engine
.eval::<()>(
r#"
let script = "for x in range(0, 500) {}";
eval(script);
"#
)
.expect_err("should error"),
EvalAltResult::ErrorTooManyOperations(_)
));
Ok(())
}

View File

@ -15,6 +15,7 @@ fn test_stack_overflow() -> Result<(), Box<EvalAltResult>> {
325
);
#[cfg(not(feature = "unchecked"))]
match engine.eval::<()>(
r"
fn foo(n) { if n == 0 { 0 } else { n + foo(n-1) } }