diff --git a/README.md b/README.md index 46bae69f..831f3765 100644 --- a/README.md +++ b/README.md @@ -13,25 +13,25 @@ to add scripting to any application. Rhai's current features set: -* Easy-to-use language similar to JS+Rust with dynamic typing but _no_ garbage collector +* 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) + 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_"`) -* Sand-boxed (the scripting [`Engine`] can be declared immutable which cannot mutate the containing environment - unless explicitly allowed via `RefCell` etc.) -* Rugged (protection against [stack-overflow](#maximum-stack-depth) and [runaway scripts](#maximum-number-of-operations) etc.) -* Able to 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) -* [Modules] -* Compiled script is [optimized](#script-optimization) for repeated evaluations -* Support for [minimal builds](#minimal-builds) by excluding unneeded language [features](#optional-features) + 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`. @@ -111,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 @@ -120,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 ``` @@ -417,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). @@ -2113,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 @@ -2258,17 +2262,17 @@ Ruggedization - protect against DoS attacks ------------------------------------------ For scripting systems open to user-land scripts, it is always best to limit the amount of resources used by a script -so that it does not crash the system by consuming all resources. +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 waiting for a result. -* **Stack**: A malignant script may consume attempt an infinite recursive call that exhausts the call stack. -* **Overflows**: A malignant script may deliberately cause numeric over-flows and/or under-flows, and/or bad - floating-point representations, in order to crash the system. -* **Files**: A malignant script may continuously `import` an external module within an infinite loop, +* **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, @@ -2288,13 +2292,14 @@ 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 a variable/constant, one operator call, one complete statement, one iteration of a loop, -or one function call etc. with sub-expressions and statement blocks executed inside these contexts accumulated on top. +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 CPU cycles, depending on the particular operation -involved. For example, loading a constant consumes very few CPU cycles, while calling an external Rust function, -though also counted as only one operation, may consume much more resources. +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. @@ -2325,7 +2330,7 @@ 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. +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 @@ -2360,7 +2365,7 @@ it detects a numeric over-flow/under-flow condition or an invalid floating-point crashing the entire system. This checking can be turned off via the [`unchecked`] feature for higher performance (but higher risks as well). -### Access to external data +### 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; diff --git a/RELEASES.md b/RELEASES.md new file mode 100644 index 00000000..cee016d7 --- /dev/null +++ b/RELEASES.md @@ -0,0 +1,66 @@ +Rhai Release Notes +================== + +Version 0.14.1 +============== + +The major features for this release is modules, script resource limits, and speed improvements +(mainly due to avoiding allocations). + +New features +------------ + +* Modules and _module resolvers_ allow loading external scripts under a module namespace. + A module can contain constant variables, Rust functions and Rhai functions. +* `export` variables and `private` functions. +* _Indexers_ for Rust types. +* Track script evaluation progress and terminate script run. +* Set limit on maximum number of operations allowed per script run. +* Set limit on maximum number of modules loaded per script run. +* A new API, `Engine::compile_scripts_with_scope`, can compile a list of script segments without needing to + first concatenate them together into one large string. +* Stepped `range` function with a custom step. + +Speed improvements +------------------ + +### `StaticVec` + +A script contains many lists - statements in a block, arguments to a function call etc. +In a typical script, most of these lists tend to be short - e.g. the vast majority of function calls contain +fewer than 4 arguments, while most statement blocks have fewer than 4-5 statements, with one or two being +the most common. Before, dynamic `Vec`'s are used to hold these short lists for very brief periods of time, +causing allocations churn. + +In this version, large amounts of allocations are avoided by converting to a `StaticVec` - +a list type based on a static array for a small number of items (currently four) - +wherever possible plus other tricks. Most real-life scripts should see material speed increases. + +### Pre-computed variable lookups + +Almost all variable lookups, as well as lookups in loaded modules, are now pre-computed. +A variable's name is almost never used to search for the variable in the current scope. + +_Getters_ and _setter_ function names are also pre-computed and cached, so no string allocations are +performed during a property get/set call. + +### Pre-computed function call hashes + +Lookup of all function calls, including Rust and Rhai ones, are now through pre-computed hashes. +The function name is no longer used to search for a function, making function call dispatches +much faster. + +### Large Boxes for expressions and statements + +The expression (`Expr`) and statement (`Stmt`) types are modified so that all of the variants contain only +one single `Box` to a large allocated structure containing _all_ the fields. This makes the `Expr` and +`Stmt` types very small (only one single pointer) and improves evaluation speed due to cache efficiency. + +Error handling +-------------- + +Previously, when an error occurs inside a function call, the error position reported is the function +call site. This makes it difficult to diagnose the actual location of the error within the function. + +A new error variant `EvalAltResult::ErrorInFunctionCall` is added in this version. +It wraps the internal error returned by the called function, including the error position within the function. diff --git a/src/engine.rs b/src/engine.rs index 4c91f39c..3febbc6f 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1768,7 +1768,7 @@ impl Engine { // Let statement Stmt::Let(x) if x.1.is_some() => { - let ((var_name, pos), expr) = x.as_ref(); + 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_to_lifetime(var_name, &state); scope.push_dynamic_value(var_name, ScopeEntryType::Normal, val, false); @@ -1776,7 +1776,7 @@ impl Engine { } Stmt::Let(x) => { - let ((var_name, pos), _) = x.as_ref(); + let ((var_name, _), _) = x.as_ref(); let var_name = unsafe_cast_var_name_to_lifetime(var_name, &state); scope.push(var_name, ()); Ok(Default::default()) @@ -1784,7 +1784,7 @@ impl Engine { // Const statement Stmt::Const(x) if x.1.is_constant() => { - let ((var_name, pos), expr) = x.as_ref(); + let ((var_name, _), expr) = x.as_ref(); let val = self.eval_expr(scope, state, &expr, level)?; let var_name = unsafe_cast_var_name_to_lifetime(var_name, &state); scope.push_dynamic_value(var_name, ScopeEntryType::Constant, val, true);