Add release notes.

This commit is contained in:
Stephen Chung 2020-05-18 09:36:34 +08:00
parent 8b5550eeb6
commit f4a528a88a
3 changed files with 106 additions and 35 deletions

View File

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

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

@ -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);