From 478bc7ab30340e36687f21b9598769e11f795e0d Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 20 Jun 2020 12:23:32 +0800 Subject: [PATCH] Change README to point to the Rhai book. --- README.md | 3073 +--------------------------------------------------- src/lib.rs | 3 +- 2 files changed, 18 insertions(+), 3058 deletions(-) diff --git a/README.md b/README.md index 008e92e7..3118724a 100644 --- a/README.md +++ b/README.md @@ -15,3072 +15,31 @@ Supported targets and builds --------------------------- * All common CPU targets for Windows, Linux and MacOS. -* [WASM] +* WebAssembly (WASM) * `no-std` Features -------- * Easy-to-use language similar to JS+Rust with dynamic typing. -* 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. +* Tight integration with native Rust [functions](https://schungx.github.io/rust/functions.html) and [types]([#custom-types-and-methods](https://schungx.github.io/rust/custom.html)), including [getters/setters](https://schungx.github.io/rust/getters-setters.html), [methods](https://schungx.github.io/rust/custom.html) and [indexers](https://schungx.github.io/rust/indexers.html). +* Freely pass Rust variables/constants into a script via an external [`Scope`](https://schungx.github.io/rust/scope.html). +* Easily [call a script-defined function](https://schungx.github.io/engine/call-fn.html) from Rust. * Fairly low compile-time overhead. * Fairly efficient evaluation (1 million iterations in 0.25 sec on a single core, 2.3 GHz Linux VM). * 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 unless explicitly permitted (e.g. via a `RefCell`). -* Rugged - protection against malicious attacks (such as [stack-overflow](#maximum-call-stack-depth), [over-sized data](#maximum-length-of-strings), and [runaway scripts](#maximum-number-of-operations) etc.) that may come from untrusted third-party user-land scripts. -* Track script evaluation [progress](#tracking-progress-and-force-terminate-script-run) and manually terminate a script run. -* [Function overloading](#function-overloading). -* [Operator overloading](#operator-overloading). -* Organize code base with dynamically-loadable [modules]. -* Scripts are [optimized](#script-optimization) (useful for template-based machine-generated scripts) for repeated evaluations. -* Support for [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`. - -**Note:** Currently, the version is `0.15.2`, so the language and API's may change before they stabilize. - -What Rhai doesn't do --------------------- - -Rhai's purpose is to provide a dynamic layer over Rust code, in the same spirit of _zero cost abstractions_. -It doesn't attempt to be a new language. For example: - -* No classes. Well, Rust doesn't either. On the other hand... -* No traits... so it is also not Rust. Do your Rusty stuff in Rust. -* No structures/records - define your types in Rust instead; Rhai can seamlessly work with _any Rust type_. - There is, however, a built-in [object map] type which is adequate for most uses. -* No first-class functions - Code your functions in Rust instead, and register them with Rhai. -* No garbage collection - this should be expected, so... -* No closures - do your closure magic in Rust instead; [turn a Rhai scripted function into a Rust closure](#calling-rhai-functions-from-rust). -* No byte-codes/JIT - Rhai has an AST-walking interpreter which will not win any speed races. The purpose of Rhai is not - to be extremely _fast_, but to make it as easy as possible to integrate with native Rust programs. - -Due to this intended usage, Rhai deliberately keeps the language simple and small by omitting advanced language features -such as classes, inheritance, first-class functions, closures, concurrency, byte-codes, JIT etc. -Avoid the temptation to write full-fledge program logic entirely in Rhai - that use case is best fulfilled by -more complete languages such as JS or Lua. - -Therefore, in actual practice, it is usually best to expose a Rust API into Rhai for scripts to call. -All your core functionalities should be in Rust. -This is similar to some dynamic languages where most of the core functionalities reside in a C/C++ standard library. - -Installation ------------- - -Install the Rhai crate on [`crates.io`](https:/crates.io/crates/rhai/) by adding this line to `dependencies`: - -```toml -[dependencies] -rhai = "0.15.2" -``` - -Use the latest released crate version on [`crates.io`](https:/crates.io/crates/rhai/): - -```toml -[dependencies] -rhai = "*" -``` - -Crate versions are released on [`crates.io`](https:/crates.io/crates/rhai/) infrequently, so to track the -latest features, enhancements and bug fixes, pull directly from GitHub: - -```toml -[dependencies] -rhai = { git = "https://github.com/jonathandturner/rhai" } -``` - -Beware that in order to use pre-releases (e.g. alpha and beta), the exact version must be specified in the `Cargo.toml`. - -Optional features ------------------ - -| Feature | Description | -| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `unchecked` | Disable arithmetic checking (such as over-flows and division by zero), call stack depth limit, operations count limit and modules loading limit. Beware that a bad script may panic the entire system! | -| `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`. | -| `no_optimize` | Disable the script optimizer. | -| `no_float` | Disable floating-point numbers and math. | -| `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_index` | Disable [arrays] and indexing features. | -| `no_object` | Disable support for custom types and [object maps]. | -| `no_function` | Disable script-defined functions. | -| `no_module` | Disable loading external modules. | -| `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | - -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 more control over what a script can (or cannot) do. - -[`unchecked`]: #optional-features -[`sync`]: #optional-features -[`no_optimize`]: #optional-features -[`no_float`]: #optional-features -[`only_i32`]: #optional-features -[`only_i64`]: #optional-features -[`no_index`]: #optional-features -[`no_object`]: #optional-features -[`no_function`]: #optional-features -[`no_module`]: #optional-features -[`no_std`]: #optional-features - -### Performance builds - -Some features are for performance. For example, using `only_i32` or `only_i64` disables all other integer types (such as `u16`). -If only a single integer type is needed in scripts - most of the time this is the case - it is best to avoid registering -lots of functions related to other integer types that will never be used. As a result, performance will improve. - -If only 32-bit integers are needed - again, most of the time this is the case - using `only_i32` disables also `i64`. -On 64-bit targets this may not gain much, but on some 32-bit targets this improves performance due to 64-bit arithmetic -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 better cache efficiency. - -### Minimal builds - -[minimal builds]: #minimal-builds - -In order to compile a _minimal_ build - i.e. a build optimized for size - perhaps for `no-std` embedded targets or for -compiling to [WASM], it is essential that 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 -``` - -Opt out of as many features as possible, if they are not needed, to reduce code size because, remember, by default -all code is compiled in as what a script requires cannot be predicted. If a language feature is not needed, -omitting them via special features is a prudent strategy to optimize the build for size. - -Omitting arrays (`no_index`) yields the most code-size savings, followed by floating-point support -(`no_float`), checked arithmetic/script resource limits (`unchecked`) and finally object maps and custom types (`no_object`). - -Where the usage scenario does not call for loading externally-defined modules, use `no_module` to save some bytes. -Disable script-defined functions (`no_function`) only when the feature is not needed because code size savings is minimal. - -[`Engine::new_raw`](#raw-engine) creates a _raw_ engine. -A _raw_ engine supports, out of the box, only a very [restricted set](#built-in-operators) of basic arithmetic and logical operators. -Selectively include other necessary functionalities by loading specific [packages] to minimize the footprint. -Packages are sharable (even across threads via the [`sync`] feature), so they only have to be created once. - -### Building to WebAssembly (WASM) - -[WASM]: #building-to-WebAssembly-wasm - -It is possible to use Rhai when compiling to WebAssembly (WASM). This yields a scripting engine (and language) -that can be run in a standard web browser. Why you would want to is another matter... as there is already -a nice, fast, complete scripting language for the the common WASM environment (i.e. a browser) - and it is called JavaScript. -But anyhow, do it because you _can_! - -When building for WASM, certain features will not be available, such as the script file API's and loading modules -from external script files. - -Also look into [minimal builds] to reduce generated WASM size. As of this version, a typical, full-featured -Rhai scripting engine compiles to a single WASM file less than 200KB gzipped. When excluding features that are -marginal in WASM environment, the gzipped payload can be further shrunk to 160KB. - -In benchmark tests, a WASM build runs scripts roughly 1.7-2.2x slower than a native optimized release build. - -Related resources ------------------ - -Other cool projects to check out: - -* [ChaiScript](http://chaiscript.com/) - A strong inspiration for Rhai. An embedded scripting language for C++ that I helped created many moons ago, now being led by my cousin. -* Check out the list of [scripting languages for Rust](https://github.com/rust-unofficial/awesome-rust#scripting) on [awesome-rust](https://github.com/rust-unofficial/awesome-rust) - -Examples --------- - -A number of examples can be found in the `examples` folder: - -| Example | Description | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| [`arrays_and_structs`](examples/arrays_and_structs.rs) | shows how to register a custom Rust type and using [arrays] on it | -| [`custom_types_and_methods`](examples/custom_types_and_methods.rs) | shows how to register a custom Rust type and methods for it | -| [`hello`](examples/hello.rs) | simple example that evaluates an expression and prints the result | -| [`no_std`](examples/no_std.rs) | example to test out `no-std` builds | -| [`reuse_scope`](examples/reuse_scope.rs) | evaluates two pieces of code in separate runs, but using a common [`Scope`] | -| [`rhai_runner`](examples/rhai_runner.rs) | runs each filename passed to it as a Rhai script | -| [`simple_fn`](examples/simple_fn.rs) | shows how to register a simple function | -| [`strings`](examples/strings.rs) | shows different ways to register functions taking string arguments | -| [`repl`](examples/repl.rs) | a simple REPL, interactively evaluate statements from stdin | - -Examples can be run with the following command: - -```bash -cargo run --example {example_name} -``` - -The `repl` example is a particularly good one as it allows one to interactively try out Rhai's -language features in a standard REPL (**R**ead-**E**val-**P**rint **L**oop). - -Example scripts ---------------- - -There are also a number of examples scripts that showcase Rhai's features, all in the `scripts` folder: - -| Language feature scripts | Description | -| ---------------------------------------------------- | ----------------------------------------------------------------------------- | -| [`array.rhai`](scripts/array.rhai) | [arrays] in Rhai | -| [`assignment.rhai`](scripts/assignment.rhai) | variable declarations | -| [`comments.rhai`](scripts/comments.rhai) | just comments | -| [`for1.rhai`](scripts/for1.rhai) | [`for`](#for-loop) loops | -| [`for2.rhai`](scripts/for2.rhai) | [`for`](#for-loop) loops on [arrays] | -| [`function_decl1.rhai`](scripts/function_decl1.rhai) | a [function] without parameters | -| [`function_decl2.rhai`](scripts/function_decl2.rhai) | a [function] with two parameters | -| [`function_decl3.rhai`](scripts/function_decl3.rhai) | a [function] with many parameters | -| [`if1.rhai`](scripts/if1.rhai) | [`if`](#if-statement) example | -| [`loop.rhai`](scripts/loop.rhai) | count-down [`loop`](#infinite-loop) in Rhai, emulating a `do` .. `while` loop | -| [`op1.rhai`](scripts/op1.rhai) | just simple addition | -| [`op2.rhai`](scripts/op2.rhai) | simple addition and multiplication | -| [`op3.rhai`](scripts/op3.rhai) | change evaluation order with parenthesis | -| [`string.rhai`](scripts/string.rhai) | [string] operations | -| [`strings_map.rhai`](scripts/strings_map.rhai) | [string] and [object map] operations | -| [`while.rhai`](scripts/while.rhai) | [`while`](#while-loop) loop | - -| Example scripts | Description | -| -------------------------------------------- | ---------------------------------------------------------------------------------- | -| [`speed_test.rhai`](scripts/speed_test.rhai) | a simple program to measure the speed of Rhai's interpreter (1 million iterations) | -| [`primes.rhai`](scripts/primes.rhai) | use Sieve of Eratosthenes to find all primes smaller than a limit | -| [`fibonacci.rhai`](scripts/fibonacci.rhai) | calculate the n-th Fibonacci number using a really dumb algorithm | -| [`mat_mul.rhai`](scripts/mat_mul.rhai) | matrix multiplication test to measure the speed of Rhai's interpreter | - -To run the scripts, either make a tiny program or use of the `rhai_runner` example: - -```bash -cargo run --example rhai_runner scripts/any_script.rhai -``` - -Hello world ------------ - -[`Engine`]: #hello-world - -To get going with Rhai, create an instance of the scripting engine via `Engine::new` and then call the `eval` method: - -```rust -use rhai::{Engine, EvalAltResult}; - -fn main() -> Result<(), Box> -{ - let engine = Engine::new(); - - let result = engine.eval::("40 + 2")?; - // ^^^^^^^ cast the result to an 'i64', this is required - - println!("Answer: {}", result); // prints 42 - - Ok(()) -} -``` - -`EvalAltResult` is a Rust `enum` containing all errors encountered during the parsing or evaluation process. - -### Script evaluation - -The type parameter is used to specify the type of the return value, which _must_ match the actual type or an error is returned. -Rhai is very strict here. Use [`Dynamic`] for uncertain return types. -There are two ways to specify the return type - _turbofish_ notation, or type inference. - -```rust -let result = engine.eval::("40 + 2")?; // return type is i64, specified using 'turbofish' notation - -let result: i64 = engine.eval("40 + 2")?; // return type is inferred to be i64 - -result.is::() == true; - -let result: Dynamic = engine.eval("boo()")?; // use 'Dynamic' if you're not sure what type it'll be! - -let result = engine.eval::("40 + 2")?; // returns an error because the actual return type is i64, not String -``` - -Evaluate a script file directly: - -```rust -let result = engine.eval_file::("hello_world.rhai".into())?; // 'eval_file' takes a 'PathBuf' -``` - -### Compiling scripts (to AST) - -To repeatedly evaluate a script, _compile_ it first into an AST (abstract syntax tree) form: - -```rust -// Compile to an AST and store it for later evaluations -let ast = engine.compile("40 + 2")?; - -for _ in 0..42 { - let result: i64 = engine.eval_ast(&ast)?; - - println!("Answer #{}: {}", i, result); // prints 42 -} -``` - -Compiling a script file is also supported: - -```rust -let ast = engine.compile_file("hello_world.rhai".into())?; -``` - -### Calling Rhai functions from Rust - -[`private`]: #calling-rhai-functions-from-rust - -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 -// Define functions in a script. -let ast = engine.compile(true, - r#" - // a function with two parameters: string and i64 - fn hello(x, y) { - x.len + y - } - - // functions can be overloaded: this one takes only one parameter - fn hello(x) { - x * 2 - } - - // this one takes no parameters - fn hello() { - 42 - } - - // this one is private and cannot be called by 'call_fn' - private hidden() { - throw "you shouldn't see me!"; - } - "#)?; - -// A custom scope can also contain any variables/constants available to the functions -let mut scope = Scope::new(); - -// Evaluate a function defined in the script, passing arguments into the script as a tuple. -// Beware, arguments must be of the correct types because Rhai does not have built-in type conversions. -// If arguments of the wrong types are passed, the Engine will not find the function. - -let result: i64 = engine.call_fn(&mut scope, &ast, "hello", ( String::from("abc"), 123_i64 ) )?; -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -// put arguments in a tuple - -let result: i64 = engine.call_fn(&mut scope, &ast, "hello", (123_i64,) )?; -// ^^^^^^^^^^ tuple of one - -let result: i64 = engine.call_fn(&mut scope, &ast, "hello", () )?; -// ^^ unit = tuple of zero - -// The following call will return a function-not-found error because -// 'hidden' is declared with 'private'. -let result: () = engine.call_fn(&mut scope, &ast, "hidden", ())?; -``` - -For more control, construct all arguments as `Dynamic` values and use `Engine::call_fn_dynamic`, passing it -anything that implements `IntoIterator` (such as a simple `Vec`): - -```rust -let result: Dynamic = engine.call_fn_dynamic(&mut scope, &ast, "hello", - vec![ String::from("abc").into(), 123_i64.into() ])?; -``` - -### Creating Rust anonymous functions from Rhai script - -[`Func`]: #creating-rust-anonymous-functions-from-rhai-script - -It is possible to further encapsulate a script in Rust such that it becomes a normal Rust function. -Such an _anonymous function_ is basically a boxed closure, very useful as call-back functions. -Creating them is accomplished via the `Func` trait which contains `create_from_script` -(as well as its companion method `create_from_ast`): - -```rust -use rhai::{Engine, Func}; // use 'Func' for 'create_from_script' - -let engine = Engine::new(); // create a new 'Engine' just for this - -let script = "fn calc(x, y) { x + y.len < 42 }"; - -// Func takes two type parameters: -// 1) a tuple made up of the types of the script function's parameters -// 2) the return type of the script function -// -// 'func' will have type Box Result>> and is callable! -let func = Func::<(i64, String), bool>::create_from_script( -// ^^^^^^^^^^^^^ function parameter types in tuple - - engine, // the 'Engine' is consumed into the closure - script, // the script, notice number of parameters must match - "calc" // the entry-point function name -)?; - -func(123, "hello".to_string())? == false; // call the anonymous function - -schedule_callback(func); // pass it as a callback to another function - -// Although there is nothing you can't do by manually writing out the closure yourself... -let engine = Engine::new(); -let ast = engine.compile(script)?; -schedule_callback(Box::new(move |x: i64, y: String| -> Result> { - engine.call_fn(&mut Scope::new(), &ast, "calc", (x, y)) -})); -``` - -Raw `Engine` ------------- - -[raw `Engine`]: #raw-engine - -`Engine::new` creates a scripting [`Engine`] with common functionalities (e.g. printing to the console via `print`). -In many controlled embedded environments, however, these are not needed. - -Use `Engine::new_raw` to create a _raw_ `Engine`, in which only a minimal set of basic arithmetic and logical operators -are supported. - -### Built-in operators - -| Operators | Assignment operators | Supported for type (see [standard types]) | -| ------------------------ | ---------------------------- | ----------------------------------------------------------------------------- | -| `+`, | `+=` | `INT`, `FLOAT` (if not [`no_float`]), `ImmutableString` | -| `-`, `*`, `/`, `%`, `~`, | `-=`, `*=`, `/=`, `%=`, `~=` | `INT`, `FLOAT` (if not [`no_float`]) | -| `<<`, `>>`, `^`, | `<<=`, `>>=`, `^=` | `INT` | -| `&`, `\|`, | `&=`, `\|=` | `INT`, `bool` | -| `&&`, `\|\|` | | `bool` | -| `==`, `!=` | | `INT`, `FLOAT` (if not [`no_float`]), `bool`, `char`, `()`, `ImmutableString` | -| `>`, `>=`, `<`, `<=` | | `INT`, `FLOAT` (if not [`no_float`]), `char`, `()`, `ImmutableString` | - -### Packages - -[package]: #packages -[packages]: #packages - -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. - -```rust -use rhai::Engine; -use rhai::packages::Package // load the 'Package' trait to use packages -use rhai::packages::CorePackage; // the 'core' package contains basic functionalities (e.g. arithmetic) - -let mut engine = Engine::new_raw(); // create a 'raw' Engine -let package = CorePackage::new(); // create a package - can be shared among multiple `Engine` instances - -engine.load_package(package.get()); // load the package manually. 'get' returns a reference to the shared package -``` - -The follow packages are available: - -| Package | Description | In `CorePackage` | In `StandardPackage` | -| ---------------------- | ------------------------------------------------------------------------------------------------------ | :--------------: | :------------------: | -| `ArithmeticPackage` | Arithmetic operators (e.g. `+`, `-`, `*`, `/`) for numeric types that are not built in (e.g. `u16`) | Yes | Yes | -| `BasicIteratorPackage` | Numeric ranges (e.g. `range(1, 10)`) | Yes | Yes | -| `LogicPackage` | Logical and comparison operators (e.g. `==`, `>`) for numeric types that are not built in (e.g. `u16`) | Yes | Yes | -| `BasicStringPackage` | Basic string functions (e.g. `print`, `debug`, `len`) that are not built in | Yes | Yes | -| `BasicTimePackage` | Basic time functions (e.g. [timestamps]) | Yes | Yes | -| `MoreStringPackage` | Additional string functions, including converting common types to string | No | Yes | -| `BasicMathPackage` | Basic math functions (e.g. `sin`, `sqrt`) | No | Yes | -| `BasicArrayPackage` | Basic [array] functions (not available under `no_index`) | No | Yes | -| `BasicMapPackage` | Basic [object map] functions (not available under `no_object`) | No | Yes | -| `EvalPackage` | Disable [`eval`] | No | No | -| `CorePackage` | Basic essentials | Yes | Yes | -| `StandardPackage` | Standard library | No | Yes | - -Packages typically contain Rust functions that are callable within a Rhai script. -All functions registered in a package is loaded under the _global namespace_ (i.e. they're available without module qualifiers). -Once a package is created (e.g. via `new`), it can be _shared_ (via `get`) among multiple instances of [`Engine`], -even across threads (under [`sync`]). 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]). -A package is _static_ (i.e. pre-loaded into an [`Engine`]), while a module is _dynamic_ (i.e. loaded with -the `import` statement). - -Custom packages can also be created. See the macro [`def_package!`](https://docs.rs/rhai/0.13.0/rhai/macro.def_package.html). - -Evaluate expressions only -------------------------- - -[`eval_expression`]: #evaluate-expressions-only -[`eval_expression_with_scope`]: #evaluate-expressions-only - -Sometimes a use case does not require a full-blown scripting _language_, but only needs to evaluate _expressions_. -In these cases, use the `compile_expression` and `eval_expression` methods or their `_with_scope` variants. - -```rust -let result = engine.eval_expression::("2 + (10 + 10) * 2")?; -``` - -When evaluating _expressions_, no full-blown statement (e.g. `if`, `while`, `for`) - not even variable assignments - -is supported and will be considered parse errors when encountered. - -```rust -// The following are all syntax errors because the script is not an expression. -engine.eval_expression::<()>("x = 42")?; -let ast = engine.compile_expression("let x = 42")?; -let result = engine.eval_expression_with_scope::(&mut scope, "if x { 42 } else { 123 }")?; -``` - -Values and types ----------------- - -[`type_of()`]: #values-and-types -[`to_string()`]: #values-and-types -[`()`]: #values-and-types -[standard types]: #values-and-types - -The following primitive types are supported natively: - -| Category | Equivalent Rust types | `type_of()` | `to_string()` | -| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------- | --------------------- | -| **Integer number** | `u8`, `i8`, `u16`, `i16`,
`u32`, `i32` (default for [`only_i32`]),
`u64`, `i64` _(default)_ | `"i32"`, `"u64"` etc. | `"42"`, `"123"` etc. | -| **Floating-point number** (disabled with [`no_float`]) | `f32`, `f64` _(default)_ | `"f32"` or `"f64"` | `"123.4567"` etc. | -| **Boolean value** | `bool` | `"bool"` | `"true"` or `"false"` | -| **Unicode character** | `char` | `"char"` | `"A"`, `"x"` etc. | -| **Immutable Unicode string** | `rhai::ImmutableString` (implemented as `Rc` or `Arc`) | `"string"` | `"hello"` etc. | -| **Array** (disabled with [`no_index`]) | `rhai::Array` | `"array"` | `"[ ?, ?, ? ]"` | -| **Object map** (disabled with [`no_object`]) | `rhai::Map` | `"map"` | `#{ "a": 1, "b": 2 }` | -| **Timestamp** (implemented in the [`BasicTimePackage`](#packages)) | `std::time::Instant` | `"timestamp"` | _not supported_ | -| **Dynamic value** (i.e. can be anything) | `rhai::Dynamic` | _the actual type_ | _actual value_ | -| **System integer** (current configuration) | `rhai::INT` (`i32` or `i64`) | `"i32"` or `"i64"` | `"42"`, `"123"` etc. | -| **System floating-point** (current configuration, disabled with [`no_float`]) | `rhai::FLOAT` (`f32` or `f64`) | `"f32"` or `"f64"` | `"123.456"` etc. | -| **Nothing/void/nil/null** (or whatever it is called) | `()` | `"()"` | `""` _(empty string)_ | - -All types are treated strictly separate by Rhai, meaning that `i32` and `i64` and `u32` are completely different - -they even cannot be added together. This is very similar to Rust. - -The default integer type is `i64`. If other integer types are not needed, it is possible to exclude them and make a -smaller build with the [`only_i64`] feature. - -If only 32-bit integers are needed, enabling the [`only_i32`] feature will remove support for all integer types other than `i32`, including `i64`. -This is useful on some 32-bit targets where using 64-bit integers incur a performance penalty. - -If no floating-point is needed or supported, use the [`no_float`] feature to remove it. - -[Strings] in Rhai are _immutable_, meaning that they can be shared but not modified. In actual, the `ImmutableString` type -is an alias to `Rc` or `Arc` (depending on the [`sync`] feature). -Any modification done to a Rhai string will cause the string to be cloned and the modifications made to the copy. - -The `to_string` function converts a standard type into a [string] for display purposes. - -The `type_of` function detects the actual type of a value. This is useful because all variables are [`Dynamic`] in nature. - -```rust -// Use 'type_of()' to get the actual types of values -type_of('c') == "char"; -type_of(42) == "i64"; - -let x = 123; -x.type_of() == "i64"; // method-call style is also OK -type_of(x) == "i64"; - -x = 99.999; -type_of(x) == "f64"; - -x = "hello"; -if type_of(x) == "string" { - do_something_with_string(x); -} -``` - -`Dynamic` values ----------------- - -[`Dynamic`]: #dynamic-values - -A `Dynamic` value can be _any_ type. However, under [`sync`], all types must be `Send + Sync`. - -Because [`type_of()`] a `Dynamic` value returns the type of the actual value, it is usually used to perform type-specific -actions based on the actual value's type. - -```rust -let mystery = get_some_dynamic_value(); - -if type_of(mystery) == "i64" { - print("Hey, I got an integer here!"); -} else if type_of(mystery) == "f64" { - print("Hey, I got a float here!"); -} else if type_of(mystery) == "string" { - print("Hey, I got a string here!"); -} else if type_of(mystery) == "bool" { - print("Hey, I got a boolean here!"); -} else if type_of(mystery) == "array" { - print("Hey, I got an array here!"); -} else if type_of(mystery) == "map" { - print("Hey, I got an object map here!"); -} else if type_of(mystery) == "TestStruct" { - print("Hey, I got the TestStruct custom type here!"); -} else { - print("I don't know what this is: " + type_of(mystery)); -} -``` - -In Rust, sometimes a `Dynamic` forms part of a returned value - a good example is an [array] with `Dynamic` elements, -or an [object map] with `Dynamic` property values. To get the _real_ values, the actual value types _must_ be known in advance. -There is no easy way for Rust to decide, at run-time, what type the `Dynamic` value is (short of using the `type_name` -function and match against the name). - -A `Dynamic` value's actual type can be checked via the `is` method. -The `cast` method then converts the value into a specific, known type. -Alternatively, use the `try_cast` method which does not panic but returns `None` when the cast fails. - -```rust -let list: Array = engine.eval("...")?; // return type is 'Array' -let item = list[0]; // an element in an 'Array' is 'Dynamic' - -item.is::() == true; // 'is' returns whether a 'Dynamic' value is of a particular type - -let value = item.cast::(); // if the element is 'i64', this succeeds; otherwise it panics -let value: i64 = item.cast(); // type can also be inferred - -let value = item.try_cast::().unwrap(); // 'try_cast' does not panic when the cast fails, but returns 'None' -``` - -The `type_name` method gets the name of the actual type as a static string slice, which can be `match`-ed against. - -```rust -let list: Array = engine.eval("...")?; // return type is 'Array' -let item = list[0]; // an element in an 'Array' is 'Dynamic' - -match item.type_name() { // 'type_name' returns the name of the actual Rust type - "i64" => ... - "alloc::string::String" => ... - "bool" => ... - "path::to::module::TestStruct" => ... -} -``` - -The following conversion traits are implemented for `Dynamic`: - -* `From` (`i32` if [`only_i32`]) -* `From` (if not [`no_float`]) -* `From` -* `From` -* `From` -* `From` -* `From>` (into an [array]) -* `From>` (into an [object map]). - -Value conversions ------------------ - -[`to_int`]: #value-conversions -[`to_float`]: #value-conversions - -The `to_float` function converts a supported number to `FLOAT` (`f32` or `f64`), -and the `to_int` function converts a supported number to `INT` (`i32` or `i64`). -That's about it. For other conversions, register custom conversion functions. - -```rust -let x = 42; -let y = x * 100.0; // <- error: cannot multiply i64 with f64 -let y = x.to_float() * 100.0; // works -let z = y.to_int() + x; // works - -let c = 'X'; // character -print("c is '" + c + "' and its code is " + c.to_int()); // prints "c is 'X' and its code is 88" -``` - -Traits ------- - -A number of traits, under the `rhai::` module namespace, provide additional functionalities. - -| Trait | Description | Methods | -| ------------------ | ---------------------------------------------------------------------------------------- | --------------------------------------- | -| `RegisterFn` | Trait for registering functions | `register_fn` | -| `RegisterResultFn` | Trait for registering fallible functions returning `Result>` | `register_result_fn` | -| `Func` | Trait for creating anonymous functions from script | `create_from_ast`, `create_from_script` | -| `ModuleResolver` | Trait implemented by module resolution services | `resolve` | - -Working with functions ----------------------- - -Rhai's scripting engine is very lightweight. It gets most of its abilities from functions. -To call these functions, they need to be registered with the [`Engine`]. - -```rust -use rhai::{Dynamic, Engine, EvalAltResult, ImmutableString}; -use rhai::RegisterFn; // use 'RegisterFn' trait for 'register_fn' -use rhai::RegisterResultFn; // use 'RegisterResultFn' trait for 'register_result_fn' - -// Normal function that returns a standard type -// Remember to use 'ImmutableString' and not 'String' -fn add_len(x: i64, s: ImmutableString) -> i64 { - x + s.len() -} -// Alternatively, '&str' maps directly to 'ImmutableString' -fn add_len_str(x: i64, s: &str) -> i64 { - x + s.len() -} - -// Function that returns a 'Dynamic' value - must return a 'Result' -fn get_any_value() -> Result> { - Ok((42_i64).into()) // standard types can use 'into()' -} - -fn main() -> Result<(), Box> -{ - let engine = Engine::new(); - - engine.register_fn("add", add_len); - engine.register_fn("add_str", add_len_str); - - let result = engine.eval::(r#"add(40, "xx")"#)?; - - println!("Answer: {}", result); // prints 42 - - let result = engine.eval::(r#"add_str(40, "xx")"#)?; - - println!("Answer: {}", result); // prints 42 - - // Functions that return Dynamic values must use register_result_fn() - engine.register_result_fn("get_any_value", get_any_value); - - let result = engine.eval::("get_any_value()")?; - - println!("Answer: {}", result); // prints 42 - - Ok(()) -} -``` - -To create a [`Dynamic`] value, use the `Dynamic::from` method. -[Standard types] in Rhai can also use `into()`. - -```rust -use rhai::Dynamic; - -let x = (42_i64).into(); // 'into()' works for standard types - -let y = Dynamic::from("hello!".to_string()); // remember &str is not supported by Rhai -``` - -Functions registered with the [`Engine`] can be _overloaded_ as long as the _signature_ is unique, -i.e. different functions can have the same name as long as their parameters are of different types -and/or different number. -New definitions _overwrite_ previous definitions of the same name and same number/types of parameters. - -### `String` parameters - -Functions accepting a parameter of `String` should use `&str` instead because it maps directly to `ImmutableString` -which is the type that Rhai uses to represent strings internally. - -```rust -fn get_len1(s: String) -> i64 { s.len() as i64 } // <- Rhai will not find this function -fn get_len2(s: &str) -> i64 { s.len() as i64 } // <- Rhai finds this function fine -fn get_len3(s: ImmutableString) -> i64 { s.len() as i64 } // <- the above is equivalent to this - -engine.register_fn("len1", get_len1); -engine.register_fn("len2", get_len2); -engine.register_fn("len3", get_len3); - -let len = engine.eval::("x.len1()")?; // error: function 'len1 (&str | ImmutableString)' not found -let len = engine.eval::("x.len2()")?; // works fine -let len = engine.eval::("x.len3()")?; // works fine -``` - -Generic functions ------------------ - -Rust generic functions can be used in Rhai, but separate instances for each concrete type must be registered separately. -This essentially overloads the function with different parameter types (Rhai does not natively support generics). - -```rust -use std::fmt::Display; - -use rhai::{Engine, RegisterFn}; - -fn show_it(x: &mut T) { - println!("put up a good show: {}!", x) -} - -fn main() -{ - let engine = Engine::new(); - - engine.register_fn("print", show_it::); - engine.register_fn("print", show_it::); - engine.register_fn("print", show_it::); -} -``` - -The above example shows how to register multiple functions (or, in this case, multiple overloaded versions of the same function) -under the same name. - -Fallible functions ------------------- - -If a function is _fallible_ (i.e. it returns a `Result<_, Error>`), it can be registered with `register_result_fn` -(using the `RegisterResultFn` trait). - -The function must return `Result>`. `Box` implements `From<&str>` and `From` etc. -and the error text gets converted into `Box`. - -The error values are `Box`-ed in order to reduce memory footprint of the error path, which should be hit rarely. - -```rust -use rhai::{Engine, EvalAltResult, Position}; -use rhai::RegisterResultFn; // use 'RegisterResultFn' trait for 'register_result_fn' - -// Function that may fail - the result type must be 'Dynamic' -fn safe_divide(x: i64, y: i64) -> Result> { - if y == 0 { - // Return an error if y is zero - Err("Division by zero!".into()) // short-cut to create Box - } else { - Ok((x / y).into()) // convert result into 'Dynamic' - } -} - -fn main() -{ - let engine = Engine::new(); - - // Fallible functions that return Result values must use register_result_fn() - engine.register_result_fn("divide", safe_divide); - - if let Err(error) = engine.eval::("divide(40, 0)") { - println!("Error: {:?}", *error); // prints ErrorRuntime("Division by zero detected!", (1, 1)") - } -} -``` - -Overriding built-in functions ----------------------------- - -Any similarly-named function defined in a script overrides any built-in function and any registered -native Rust function of the same name and number of parameters. - -```rust -// Override the built-in function 'to_int' -fn to_int(num) { - print("Ha! Gotcha! " + num); -} - -print(to_int(123)); // what happens? -``` - -A registered function, in turn, overrides any built-in function of the same name and number/types of parameters. - -Operator overloading --------------------- - -In Rhai, a lot of functionalities are actually implemented as functions, including basic operations such as arithmetic calculations. -For example, in the expression "`a + b`", the `+` operator is _not_ built in, but calls a function named "`+`" instead! - -```rust -let x = a + b; -let x = +(a, b); // <- the above is equivalent to this function call -``` - -Similarly, comparison operators including `==`, `!=` etc. are all implemented as functions, with the stark exception of `&&` and `||`. -Because they [_short-circuit_](#boolean-operators), `&&` and `||` are handled specially and _not_ via a function; as a result, -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 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 overrides the built-in version. - -```rust -use rhai::{Engine, EvalAltResult, RegisterFn}; - -let mut engine = Engine::new(); - -fn strange_add(a: i64, b: i64) -> i64 { (a + b) * 42 } - -engine.register_fn("+", strange_add); // overload '+' operator for two integers! - -let result: i64 = engine.eval("1 + 0"); // the overloading version is used - -println!("result: {}", result); // prints 42 - -let result: f64 = engine.eval("1.0 + 0.0"); // '+' operator for two floats not overloaded - -println!("result: {}", result); // prints 1.0 - -fn mixed_add(a: i64, b: f64) -> f64 { (a as f64) + b } - -engine.register_fn("+", mixed_add); // register '+' operator for an integer and a float - -let result: i64 = engine.eval("1 + 1.0"); // prints 2.0 (normally an error) -``` - -Use operator overloading for custom types (described below) only. -Be very careful when overriding built-in operators because script authors expect standard operators to behave in a -consistent and predictable manner, and will be annoyed if a calculation for '`+`' turns into a subtraction, for example. - -Operator overloading also impacts script optimization when using [`OptimizationLevel::Full`]. -See the [relevant section](#script-optimization) for more details. - -Custom types and methods ------------------------ - -A more complete example of working with Rust: - -```rust -use rhai::{Engine, EvalAltResult}; -use rhai::RegisterFn; - -#[derive(Clone)] -struct TestStruct { - field: i64 -} - -impl TestStruct { - fn update(&mut self) { - self.field += 41; - } - - fn new() -> Self { - TestStruct { field: 1 } - } -} - -fn main() -> Result<(), Box> -{ - let engine = Engine::new(); - - engine.register_type::(); - - engine.register_fn("update", TestStruct::update); - engine.register_fn("new_ts", TestStruct::new); - - let result = engine.eval::("let x = new_ts(); x.update(); x")?; - - println!("result: {}", result.field); // prints 42 - - Ok(()) -} -``` - -All custom types must implement `Clone` as this allows the [`Engine`] to pass by value. -Support for custom types can be turned off via the [`no_object`] feature. - -```rust -#[derive(Clone)] -struct TestStruct { - field: i64 -} -``` - -Next, create a few methods for later use in scripts. -Notice that the custom type needs to be _registered_ with the [`Engine`]. - -```rust -impl TestStruct { - fn update(&mut self) { // methods take &mut as first parameter - self.field += 41; - } - - fn new() -> Self { - TestStruct { field: 1 } - } -} - -let engine = Engine::new(); - -engine.register_type::(); -``` - -To use native types, methods and functions with the [`Engine`], simply register them using one of the `Engine::register_XXX` API. -Below, the `update` and `new` methods are registered using `Engine::register_fn`. - -***Note**: Rhai follows the convention that methods of custom types take a `&mut` first parameter so that invoking methods -can update the custom types. All other parameters in Rhai are passed by value (i.e. clones).* - -```rust -engine.register_fn("update", TestStruct::update); // registers 'update(&mut TestStruct)' -engine.register_fn("new_ts", TestStruct::new); // registers 'new()' -``` - -The custom type is then ready for use in scripts. Scripts can see the functions and methods registered earlier. -Get the evaluation result back out from script-land just as before, this time casting to the custom type: - -```rust -let result = engine.eval::("let x = new_ts(); x.update(); x")?; - -println!("result: {}", result.field); // prints 42 -``` - -In fact, any function with a first argument that is a `&mut` reference can be used as method calls because -internally they are the same thing: methods on a type is implemented as a functions taking a `&mut` first argument. - -```rust -fn foo(ts: &mut TestStruct) -> i64 { - ts.field -} - -engine.register_fn("foo", foo); // register ad hoc function with correct signature - -let result = engine.eval::( - "let x = new_ts(); x.foo()" // 'foo' can be called like a method on 'x' -)?; - -println!("result: {}", result); // prints 1 -``` - -Under [`no_object`], however, the _method_ style of function calls (i.e. calling a function as an object-method) -is no longer supported. - -```rust -// Below is a syntax error under 'no_object' because 'clear' cannot be called in method style. -let result = engine.eval::<()>("let x = [1, 2, 3]; x.clear()")?; -``` - -[`type_of()`] works fine with custom types and returns the name of the type. -If `Engine::register_type_with_name` is used to register the custom type -with a special "pretty-print" name, [`type_of()`] will return that name instead. - -```rust -engine.register_type::(); -engine.register_fn("new_ts", TestStruct::new); -let x = new_ts(); -print(x.type_of()); // prints "path::to::module::TestStruct" - -engine.register_type_with_name::("Hello"); -engine.register_fn("new_ts", TestStruct::new); -let x = new_ts(); -print(x.type_of()); // prints "Hello" -``` - -### Getters and setters - -Similarly, custom types can expose members by registering a `get` and/or `set` function. - -```rust -#[derive(Clone)] -struct TestStruct { - field: String -} - -impl TestStruct { - // Returning a 'String' is OK - Rhai converts it into 'ImmutableString' - fn get_field(&mut self) -> String { - self.field.clone() - } - - // Remember Rhai uses 'ImmutableString' or '&str' instead of 'String' - fn set_field(&mut self, new_val: ImmutableString) { - // Get a 'String' from an 'ImmutableString' - self.field = (*new_val).clone(); - } - - fn new() -> Self { - TestStruct { field: "hello" } - } -} - -let engine = Engine::new(); - -engine.register_type::(); - -engine.register_get_set("xyz", TestStruct::get_field, TestStruct::set_field); -engine.register_fn("new_ts", TestStruct::new); - -// Return result can be 'String' - Rhai will automatically convert it from 'ImmutableString' -let result = engine.eval::(r#"let a = new_ts(); a.xyz = "42"; a.xyz"#)?; - -println!("Answer: {}", result); // prints 42 -``` - -### Indexers - -Custom types can also expose an _indexer_ by registering an indexer function. -A custom type with an indexer function defined can use the bracket '`[]`' notation to get a property value - -```rust -#[derive(Clone)] -struct TestStruct { - fields: Vec -} - -impl TestStruct { - fn get_field(&mut self, index: i64) -> i64 { - self.fields[index as usize] - } - fn set_field(&mut self, index: i64, value: i64) { - self.fields[index as usize] = value - } - - fn new() -> Self { - TestStruct { fields: vec![1, 2, 3, 4, 5] } - } -} - -let engine = Engine::new(); - -engine.register_type::(); - -engine.register_fn("new_ts", TestStruct::new); - -// Shorthand: engine.register_indexer_get_set(TestStruct::get_field, TestStruct::set_field); -engine.register_indexer_get(TestStruct::get_field); -engine.register_indexer_set(TestStruct::set_field); - -let result = engine.eval::("let a = new_ts(); a[2] = 42; a[2]")?; - -println!("Answer: {}", result); // prints 42 -``` - -For efficiency reasons, indexers **cannot** be used to overload (i.e. override) built-in indexing operations for -[arrays] and [object maps]. - -### Disabling custom types - -The custom types API `register_type`, `register_type_with_name`, `register_get`, `register_set`, `register_get_set`, -`register_indexer_get`, `register_indexer_set` and `register_indexer_get_set` are not available under [`no_object`]. - -The indexers API `register_indexer_get`, `register_indexer_set` and `register_indexer_get_set` are also -not available under [`no_index`]. - -### Printing for custom types - -To use custom types for `print` and `debug`, or convert its value into a [string], it is necessary that the following -functions be registered (assuming the custom type is `T : Display + Debug`): - -| Function | Signature | Typical implementation | Usage | -| ----------- | ------------------------------------------------ | ------------------------------------- | --------------------------------------------------------------------------------------- | -| `to_string` | `|s: &mut T| -> ImmutableString` | `s.to_string().into()` | Converts the custom type into a [string] | -| `print` | `|s: &mut T| -> ImmutableString` | `s.to_string().into()` | Converts the custom type into a [string] for the [`print`](#print-and-debug) statement | -| `debug` | `|s: &mut T| -> ImmutableString` | `format!("{:?}", s).into()` | Converts the custom type into a [string] for the [`debug`](#print-and-debug) statement | -| `+` | `|s1: ImmutableString, s: T| -> ImmutableString` | `s1 + s` | Append the custom type to another [string], for `print("Answer: " + type);` usage | -| `+` | `|s: T, s2: ImmutableString| -> ImmutableString` | `s.to_string().push_str(&s2).into();` | Append another [string] to the custom type, for `print(type + " is the answer");` usage | -| `+=` | `|s1: &mut ImmutableString, s: T|` | `s1 += s.to_string()` | Append the custom type to an existing [string], for `s += type;` usage | - -`Scope` - Initializing and maintaining state -------------------------------------------- - -[`Scope`]: #scope---initializing-and-maintaining-state - -By default, Rhai treats each [`Engine`] invocation as a fresh one, persisting only the functions that have been defined -but no global state. This gives each evaluation a clean starting slate. In order to continue using the same global state -from one invocation to the next, such a state must be manually created and passed in. - -All `Scope` variables are [`Dynamic`], meaning they can store values of any type. Under [`sync`], however, -only types that are `Send + Sync` are supported, and the entire `Scope` itself will also be `Send + Sync`. -This is extremely useful in multi-threaded applications. - -In this example, a global state object (a `Scope`) is created with a few initialized variables, then the same state is -threaded through multiple invocations: - -```rust -use rhai::{Engine, Scope, EvalAltResult}; - -fn main() -> Result<(), Box> -{ - let engine = Engine::new(); - - // First create the state - let mut scope = Scope::new(); - - // Then push (i.e. add) some initialized variables into the state. - // Remember the system number types in Rhai are i64 (i32 if 'only_i32') ond f64. - // Better stick to them or it gets hard working with the script. - scope.push("y", 42_i64); - scope.push("z", 999_i64); - - // 'set_value' adds a variable when one doesn't exist - scope.set_value("s", "hello, world!".to_string()); // remember to use 'String', not '&str' - - // First invocation - engine.eval_with_scope::<()>(&mut scope, r" - let x = 4 + 5 - y + z + s.len; - y = 1; - ")?; - - // Second invocation using the same state - let result = engine.eval_with_scope::(&mut scope, "x")?; - - println!("result: {}", result); // prints 979 - - // Variable y is changed in the script - read it with 'get_value' - assert_eq!(scope.get_value::("y").expect("variable y should exist"), 1); - - // We can modify scope variables directly with 'set_value' - scope.set_value("y", 42_i64); - assert_eq!(scope.get_value::("y").expect("variable y should exist"), 42); - - Ok(()) -} -``` - -Engine configuration options ---------------------------- - -| Method | Not available under | Description | -| ------------------------ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| `set_optimization_level` | [`no_optimize`] | Set the amount of script _optimizations_ performed. See [script optimization]. | -| `set_max_expr_depths` | [`unchecked`] | Set the maximum nesting levels of an expression/statement. See [maximum statement depth](#maximum-statement-depth). | -| `set_max_call_levels` | [`unchecked`] | Set the maximum number of function call levels (default 50) to avoid infinite recursion. See [maximum call stack depth](#maximum-call-stack-depth). | -| `set_max_operations` | [`unchecked`] | Set the maximum number of _operations_ that a script is allowed to consume. See [maximum number of operations](#maximum-number-of-operations). | -| `set_max_modules` | [`unchecked`] | Set the maximum number of [modules] that a script is allowed to load. See [maximum number of modules](#maximum-number-of-modules). | -| `set_max_string_size` | [`unchecked`] | Set the maximum length (in UTF-8 bytes) for [strings]. See [maximum length of strings](#maximum-length-of-strings). | -| `set_max_array_size` | [`unchecked`], [`no_index`] | Set the maximum size for [arrays]. See [maximum size of arrays](#maximum-size-of-arrays). | -| `set_max_map_size` | [`unchecked`], [`no_object`] | Set the maximum number of properties for [object maps]. See [maximum size of object maps](#maximum-size-of-object-maps). | - -------- - -Rhai Language Guide -=================== - -Comments --------- - -Comments are C-style, including '`/*` ... `*/`' pairs and '`//`' for comments to the end of the line. -Comments can be nested. - -```rust -let /* intruder comment */ name = "Bob"; - -// This is a very important comment - -/* This comment spans - multiple lines, so it - only makes sense that - it is even more important */ - -/* Fear not, Rhai satisfies all nesting needs with nested comments: - /*/*/*/*/**/*/*/*/*/ -*/ -``` - -Keywords --------- - -The following are reserved keywords in Rhai: - -| Keywords | Usage | Not available under feature | -| ------------------------------------------------- | --------------------- | :-------------------------: | -| `true`, `false` | Boolean constants | | -| `let`, `const` | Variable declarations | | -| `if`, `else` | Control flow | | -| `while`, `loop`, `for`, `in`, `continue`, `break` | Looping | | -| `fn`, `private` | Functions | [`no_function`] | -| `return` | Return values | | -| `throw` | Return errors | | -| `import`, `export`, `as` | Modules | [`no_module`] | - -Keywords cannot be the name of a [function] or [variable], unless the relevant exclusive feature is enabled. -For example, `fn` is a valid variable name under [`no_function`]. - -Statements ----------- - -Statements are terminated by semicolons '`;`' and they are mandatory, -except for the _last_ statement in a _block_ (enclosed by '`{`' .. '`}`' pairs) where it can be omitted. - -A statement can be used anywhere where an expression is expected. These are called, for lack of a more -creative name, "statement expressions." The _last_ statement of a statement block is _always_ the block's -return value when used as a statement. -If the last statement has no return value (e.g. variable definitions, assignments) then it is assumed to be [`()`]. - -```rust -let a = 42; // normal assignment statement -let a = foo(42); // normal function call statement -foo < 42; // normal expression as statement - -let a = { 40 + 2 }; // 'a' is set to the value of the statement block, which is the value of the last statement -// ^ the last statement does not require a terminating semicolon (although it also works with it) -// ^ semicolon required here to terminate the assignment statement; it is a syntax error without it - -4 * 10 + 2 // a statement which is just one expression; no ending semicolon is OK - // because it is the last statement of the whole block -``` - -Variables ---------- - -[variable]: #variables -[variables]: #variables - -Variables in Rhai follow normal C naming rules (i.e. must contain only ASCII letters, digits and underscores '`_`'). - -Variable names must start with an ASCII letter or an underscore '`_`', must contain at least one ASCII letter, -and must start with an ASCII letter before a digit. -Therefore, names like '`_`', '`_42`', '`3a`' etc. are not legal variable names, but '`_c3po`' and '`r2d2`' are. -Variable names are also case _sensitive_. - -Variables are defined using the `let` keyword. A variable defined within a statement block is _local_ to that block. - -```rust -let x = 3; // ok -let _x = 42; // ok -let x_ = 42; // also ok -let _x_ = 42; // still ok - -let _ = 123; // <- syntax error: illegal variable name -let _9 = 9; // <- syntax error: illegal variable name - -let x = 42; // variable is 'x', lower case -let X = 123; // variable is 'X', upper case -x == 42; -X == 123; - -{ - let x = 999; // local variable 'x' shadows the 'x' in parent block - x == 999; // access to local 'x' -} -x == 42; // the parent block's 'x' is not changed -``` - -Constants ---------- - -Constants can be defined using the `const` keyword and are immutable. Constants follow the same naming rules as [variables]. - -```rust -const x = 42; -print(x * 2); // prints 84 -x = 123; // <- syntax error: cannot assign to constant -``` - -Constants must be assigned a _value_, not an expression. - -```rust -const x = 40 + 2; // <- syntax error: cannot assign expression to constant -``` - -Numbers -------- - -Integer numbers follow C-style format with support for decimal, binary ('`0b`'), octal ('`0o`') and hex ('`0x`') notations. - -The default system integer type (also aliased to `INT`) is `i64`. It can be turned into `i32` via the [`only_i32`] feature. - -Floating-point numbers are also supported if not disabled with [`no_float`]. The default system floating-point type is `i64` -(also aliased to `FLOAT`). - -'`_`' separators can be added freely and are ignored within a number. - -| Format | Type | -| ---------------- | ---------------- | -| `123_345`, `-42` | `i64` in decimal | -| `0o07_76` | `i64` in octal | -| `0xabcd_ef` | `i64` in hex | -| `0b0101_1001` | `i64` in binary | -| `123_456.789` | `f64` | - -Numeric operators ------------------ - -Numeric operators generally follow C styles. - -| Operator | Description | Integers only | -| -------- | ---------------------------------------------------- | :-----------: | -| `+` | Plus | | -| `-` | Minus | | -| `*` | Multiply | | -| `/` | Divide (integer division if acting on integer types) | | -| `%` | Modulo (remainder) | | -| `~` | Power | | -| `&` | Binary _And_ bit-mask | Yes | -| `\|` | Binary _Or_ bit-mask | Yes | -| `^` | Binary _Xor_ bit-mask | Yes | -| `<<` | Left bit-shift | Yes | -| `>>` | Right bit-shift | Yes | - -```rust -let x = (1 + 2) * (6 - 4) / 2; // arithmetic, with parentheses -let reminder = 42 % 10; // modulo -let power = 42 ~ 2; // power (i64 and f64 only) -let left_shifted = 42 << 3; // left shift -let right_shifted = 42 >> 3; // right shift -let bit_op = 42 | 99; // bit masking -``` - -Unary operators ---------------- - -| Operator | Description | -| -------- | ----------- | -| `+` | Plus | -| `-` | Negative | - -```rust -let number = -5; -number = -5 - +5; -``` - -Numeric functions ------------------ - -The following standard functions (defined in the [`BasicMathPackage`](#packages) but excluded if using a [raw `Engine`]) operate on -`i8`, `i16`, `i32`, `i64`, `f32` and `f64` only: - -| Function | Description | -| ------------ | --------------------------------- | -| `abs` | absolute value | -| [`to_float`] | converts an integer type to `f64` | - -Floating-point functions ------------------------- - -The following standard functions (defined in the [`BasicMathPackage`](#packages) but excluded if using a [raw `Engine`]) operate on `f64` only: - -| Category | Functions | -| ---------------- | --------------------------------------------------------------------- | -| Trigonometry | `sin`, `cos`, `tan`, `sinh`, `cosh`, `tanh` in degrees | -| Arc-trigonometry | `asin`, `acos`, `atan`, `asinh`, `acosh`, `atanh` in degrees | -| Square root | `sqrt` | -| Exponential | `exp` (base _e_) | -| Logarithmic | `ln` (base _e_), `log10` (base 10), `log` (any base) | -| Rounding | `floor`, `ceiling`, `round`, `int`, `fraction` methods and properties | -| Conversion | [`to_int`] | -| Testing | `is_nan`, `is_finite`, `is_infinite` methods and properties | - -Strings and Chars ------------------ - -[string]: #strings-and-chars -[strings]: #strings-and-chars -[char]: #strings-and-chars - -All strings in Rhai are implemented as `ImmutableString` (see [standard types]). -`ImmutableString` should be used in place of the standard Rust type `String` when registering functions. - -String and character literals follow C-style formatting, with support for Unicode ('`\u`_xxxx_' or '`\U`_xxxxxxxx_') -and hex ('`\x`_xx_') escape sequences. - -Hex sequences map to ASCII characters, while '`\u`' maps to 16-bit common Unicode code points and '`\U`' maps the full, -32-bit extended Unicode code points. - -Standard escape sequences: - -| Escape sequence | Meaning | -| --------------- | ------------------------------ | -| `\\` | back-slash `\` | -| `\t` | tab | -| `\r` | carriage-return `CR` | -| `\n` | line-feed `LF` | -| `\"` | double-quote `"` in strings | -| `\'` | single-quote `'` in characters | -| `\x`_xx_ | Unicode in 2-digit hex | -| `\u`_xxxx_ | Unicode in 4-digit hex | -| `\U`_xxxxxxxx_ | Unicode in 8-digit hex | - -Internally Rhai strings are stored as UTF-8 just like Rust (they _are_ Rust `String`'s!), but there are major differences. -In Rhai a string is the same as an array of Unicode characters and can be directly indexed (unlike Rust). -This is similar to most other languages where strings are internally represented not as UTF-8 but as arrays of multi-byte -Unicode characters. -Individual characters within a Rhai string can also be replaced just as if the string is an array of Unicode characters. -In Rhai, there is also no separate concepts of `String` and `&str` as in Rust. - -Rhai strings are _immutable_ and can be shared. -Modifying a Rhai string actually causes it first to be cloned, and then the modification made to the copy. - -Strings can be built up from other strings and types via the `+` operator (provided by the [`MoreStringPackage`](#packages) -but excluded if using a [raw `Engine`]). This is particularly useful when printing output. - -[`type_of()`] a string returns `"string"`. - -```rust -let name = "Bob"; -let middle_initial = 'C'; -let last = "Davis"; - -let full_name = name + " " + middle_initial + ". " + last; -full_name == "Bob C. Davis"; - -// String building with different types -let age = 42; -let record = full_name + ": age " + age; -record == "Bob C. Davis: age 42"; - -// Unlike Rust, Rhai strings can be indexed to get a character -// (disabled with 'no_index') -let c = record[4]; -c == 'C'; - -ts.s = record; // custom type properties can take strings - -let c = ts.s[4]; -c == 'C'; - -let c = "foo"[0]; // indexing also works on string literals... -c == 'f'; - -let c = ("foo" + "bar")[5]; // ... and expressions returning strings -c == 'r'; - -// Escape sequences in strings -record += " \u2764\n"; // escape sequence of '❤' in Unicode -record == "Bob C. Davis: age 42 ❤\n"; // '\n' = new-line - -// Unlike Rust, Rhai strings can be directly modified character-by-character -// (disabled with 'no_index') -record[4] = '\x58'; // 0x58 = 'X' -record == "Bob X. Davis: age 42 ❤\n"; - -// Use 'in' to test if a substring (or character) exists in a string -"Davis" in record == true; -'X' in record == true; -'C' in record == false; - -// Strings can be iterated with a 'for' statement, yielding characters -for ch in record { - print(ch); -} -``` - -The maximum allowed length of a string can be controlled via `Engine::set_max_string_size` -(see [maximum length of strings](#maximum-length-of-strings)). - -### Built-in functions - -The following standard methods (mostly defined in the [`MoreStringPackage`](#packages) but excluded if using a [raw `Engine`]) operate on strings: - -| Function | Parameter(s) | Description | -| ------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | -| `len` method and property | _none_ | returns the number of characters (not number of bytes) in the string | -| `pad` | character to pad, target length | pads the string with an character to at least a specified length | -| `+=` operator, `append` | character/string to append | Adds a character or a string to the end of another string | -| `clear` | _none_ | empties the string | -| `truncate` | target length | cuts off the string at exactly a specified number of characters | -| `contains` | character/sub-string to search for | checks if a certain character or sub-string occurs in the string | -| `index_of` | character/sub-string to search for, start index _(optional)_ | returns the index that a certain character or sub-string occurs in the string, or -1 if not found | -| `sub_string` | start index, length _(optional)_ | extracts a sub-string (to the end of the string if length is not specified) | -| `crop` | start index, length _(optional)_ | retains only a portion of the string (to the end of the string if length is not specified) | -| `replace` | target character/sub-string, replacement character/string | replaces a sub-string with another | -| `trim` | _none_ | trims the string of whitespace at the beginning and end | - -### Examples - -```rust -let full_name == " Bob C. Davis "; -full_name.len == 14; - -full_name.trim(); -full_name.len == 12; -full_name == "Bob C. Davis"; - -full_name.pad(15, '$'); -full_name.len == 15; -full_name == "Bob C. Davis$$$"; - -let n = full_name.index_of('$'); -n == 12; - -full_name.index_of("$$", n + 1) == 13; - -full_name.sub_string(n, 3) == "$$$"; - -full_name.truncate(6); -full_name.len == 6; -full_name == "Bob C."; - -full_name.replace("Bob", "John"); -full_name.len == 7; -full_name == "John C."; - -full_name.contains('C') == true; -full_name.contains("John") == true; - -full_name.crop(5); -full_name == "C."; - -full_name.crop(0, 1); -full_name == "C"; - -full_name.clear(); -full_name.len == 0; -``` - -Arrays ------- - -[array]: #arrays -[arrays]: #arrays -[`Array`]: #arrays - -Arrays are first-class citizens in Rhai. Like C, arrays are accessed with zero-based, non-negative integer indices. -Array literals are built within square brackets '`[`' ... '`]`' and separated by commas '`,`'. -All elements stored in an array are [`Dynamic`], and the array can freely grow or shrink with elements added or removed. - -The Rust type of a Rhai array is `rhai::Array`. [`type_of()`] an array returns `"array"`. - -Arrays are disabled via the [`no_index`] feature. - -### Built-in functions - -The following methods (mostly defined in the [`BasicArrayPackage`](#packages) but excluded if using a [raw `Engine`]) operate on arrays: - -| Function | Parameter(s) | Description | -| ------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| `push` | element to insert | inserts an element at the end | -| `+=` operator, `append` | array to append | concatenates the second array to the end of the first | -| `+` operator | first array, second array | concatenates the first array with the second | -| `insert` | element to insert, position
(beginning if <= 0, end if >= length) | insert an element at a certain index | -| `pop` | _none_ | removes the last element and returns it ([`()`] if empty) | -| `shift` | _none_ | removes the first element and returns it ([`()`] if empty) | -| `remove` | index | removes an element at a particular index and returns it, or returns [`()`] if the index is not valid | -| `len` method and property | _none_ | returns the number of elements | -| `pad` | element to pad, target length | pads the array with an element to at least a specified length | -| `clear` | _none_ | empties the array | -| `truncate` | target length | cuts off the array at exactly a specified length (discarding all subsequent elements) | - -### Examples - -```rust -let y = [2, 3]; // array literal with 2 elements - -let y = [2, 3,]; // trailing comma is OK - -y.insert(0, 1); // insert element at the beginning -y.insert(999, 4); // insert element at the end - -y.len == 4; - -y[0] == 1; -y[1] == 2; -y[2] == 3; -y[3] == 4; - -(1 in y) == true; // use 'in' to test if an item exists in the array -(42 in y) == false; // 'in' uses the '==' operator (which users can override) - // to check if the target item exists in the array - -y[1] = 42; // array elements can be reassigned - -(42 in y) == true; - -y.remove(2) == 3; // remove element - -y.len == 3; - -y[2] == 4; // elements after the removed element are shifted - -ts.list = y; // arrays can be assigned completely (by value copy) -let foo = ts.list[1]; -foo == 42; - -let foo = [1, 2, 3][0]; -foo == 1; - -fn abc() { - [42, 43, 44] // a function returning an array -} - -let foo = abc()[0]; -foo == 42; - -let foo = y[0]; -foo == 1; - -y.push(4); // 4 elements -y.push(5); // 5 elements - -y.len == 5; - -let first = y.shift(); // remove the first element, 4 elements remaining -first == 1; - -let last = y.pop(); // remove the last element, 3 elements remaining -last == 5; - -y.len == 3; - -for item in y { // arrays can be iterated with a 'for' statement - print(item); -} - -y.pad(10, "hello"); // pad the array up to 10 elements - -y.len == 10; - -y.truncate(5); // truncate the array to 5 elements - -y.len == 5; - -y.clear(); // empty the array - -y.len == 0; -``` - -`push` and `pad` are only defined for standard built-in types. For custom types, type-specific versions must be registered: - -```rust -engine.register_fn("push", |list: &mut Array, item: MyType| list.push(Box::new(item)) ); -``` - -The maximum allowed size of an array can be controlled via `Engine::set_max_array_size` -(see [maximum size of arrays](#maximum-size-of-arrays)). - -Object maps ------------ - -[`Map`]: #object-maps -[object map]: #object-maps -[object maps]: #object-maps - -Object maps are dictionaries. Properties are all [`Dynamic`] and can be freely added and retrieved. -Object map literals are built within braces '`#{`' ... '`}`' (_name_ `:` _value_ syntax similar to Rust) -and separated by commas '`,`'. The property _name_ can be a simple variable name following the same -naming rules as [variables], or an arbitrary [string] literal. - -Property values can be accessed via the dot notation (_object_ `.` _property_) or index notation (_object_ `[` _property_ `]`). -The dot notation allows only property names that follow the same naming rules as [variables]. -The index notation allows setting/getting properties of arbitrary names (even the empty [string]). - -**Important:** Trying to read a non-existent property returns [`()`] instead of causing an error. - -The Rust type of a Rhai object map is `rhai::Map`. [`type_of()`] an object map returns `"map"`. - -Object maps are disabled via the [`no_object`] feature. - -### Built-in functions - -The following methods (defined in the [`BasicMapPackage`](#packages) but excluded if using a [raw `Engine`]) operate on object maps: - -| Function | Parameter(s) | Description | -| ---------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -| `has` | property name | does the object map contain a property of a particular name? | -| `len` | _none_ | returns the number of properties | -| `clear` | _none_ | empties the object map | -| `remove` | property name | removes a certain property and returns it ([`()`] if the property does not exist) | -| `+=` operator, `mixin` | second object map | mixes in all the properties of the second object map to the first (values of properties with the same names replace the existing values) | -| `+` operator | first object map, second object map | merges the first object map with the second | -| `keys` | _none_ | returns an [array] of all the property names (in random order), not available under [`no_index`] | -| `values` | _none_ | returns an [array] of all the property values (in random order), not available under [`no_index`] | - -### Examples - -```rust -let y = #{ // object map literal with 3 properties - a: 1, - bar: "hello", - "baz!$@": 123.456, // like JS, you can use any string as property names... - "": false, // even the empty string! - - a: 42 // <- syntax error: duplicated property name -}; - -y.a = 42; // access via dot notation -y.baz!$@ = 42; // <- syntax error: only proper variable names allowed in dot notation -y."baz!$@" = 42; // <- syntax error: strings not allowed in dot notation - -y.a == 42; - -y["baz!$@"] == 123.456; // access via index notation - -"baz!$@" in y == true; // use 'in' to test if a property exists in the object map -("z" in y) == false; - -ts.obj = y; // object maps can be assigned completely (by value copy) -let foo = ts.list.a; -foo == 42; - -let foo = #{ a:1,}; // trailing comma is OK - -let foo = #{ a:1, b:2, c:3 }["a"]; -foo == 1; - -fn abc() { - #{ a:1, b:2, c:3 } // a function returning an object map -} - -let foo = abc().b; -foo == 2; - -let foo = y["a"]; -foo == 42; - -y.has("a") == true; -y.has("xyz") == false; - -y.xyz == (); // a non-existing property returns '()' -y["xyz"] == (); - -y.len() == 3; - -y.remove("a") == 1; // remove property - -y.len() == 2; -y.has("a") == false; - -for name in keys(y) { // get an array of all the property names via the 'keys' function - print(name); -} - -for val in values(y) { // get an array of all the property values via the 'values' function - print(val); -} - -y.clear(); // empty the object map - -y.len() == 0; -``` - -The maximum allowed size of an object map can be controlled via `Engine::set_max_map_size` -(see [maximum size of object maps](#maximum-size-of-object-maps)). - -### Parsing from JSON - -The syntax for an object map is extremely similar to JSON, with the exception of `null` values which can -technically be mapped to [`()`]. A valid JSON string does not start with a hash character `#` while a -Rhai object map does - that's the major difference! - -JSON numbers are all floating-point while Rhai supports integers (`INT`) and floating-point (`FLOAT`) if -the [`no_float`] feature is not enabled. Most common generators of JSON data distinguish between -integer and floating-point values by always serializing a floating-point number with a decimal point -(i.e. `123.0` instead of `123` which is assumed to be an integer). This style can be used successfully -with Rhai object maps. - -Use the `parse_json` method to parse a piece of JSON into an object map: - -```rust -// JSON string - notice that JSON property names are always quoted -// notice also that comments are acceptable within the JSON string -let json = r#"{ - "a": 1, // <- this is an integer number - "b": true, - "c": 123.0, // <- this is a floating-point number - "$d e f!": "hello", // <- any text can be a property name - "^^^!!!": [1,42,"999"], // <- value can be array or another hash - "z": null // <- JSON 'null' value - } -"#; - -// Parse the JSON expression as an object map -// Set the second boolean parameter to true in order to map 'null' to '()' -let map = engine.parse_json(json, true)?; - -map.len() == 6; // 'map' contains all properties in the JSON string - -// Put the object map into a 'Scope' -let mut scope = Scope::new(); -scope.push("map", map); - -let result = engine.eval_with_scope::(r#"map["^^^!!!"].len()"#)?; - -result == 3; // the object map is successfully used in the script -``` - -`timestamp`'s +* 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 unless explicitly permitted (e.g. via a `RefCell`). +* Rugged - protection against malicious attacks (such as [stack-overflow](https://schungx.github.io/safety/max-call-stack.html), [over-sized data](https://schungx.github.io/safety/max-string-size.html), and [runaway scripts](https://schungx.github.io/safety/max-operations.html) etc.) that may come from untrusted third-party user-land scripts. +* Track script evaluation [progress](https://schungx.github.io/safety/progress.html) and manually terminate a script run. +* [Function overloading](https://schungx.github.io/language/overload.html). +* [Operator overloading](https://schungx.github.io/rust/operators.html). +* Organize code base with dynamically-loadable [modules](https://schungx.github.io/language/modules.html). +* Scripts are [optimized](https://schungx.github.io/engine/optimize.html) (useful for template-based machine-generated scripts) for repeated evaluations. +* Support for [minimal builds](https://schungx.github.io/start/builds/minimal.html) by excluding unneeded language [features](https://schungx.github.io/start/features.html). + +Documentation ------------- -[`timestamp`]: #timestamps -[timestamp]: #timestamps -[timestamps]: #timestamps - -Timestamps are provided by the [`BasicTimePackage`](#packages) (excluded if using a [raw `Engine`]) via the `timestamp` -function. - -The Rust type of a timestamp is `std::time::Instant`. [`type_of()`] a timestamp returns `"timestamp"`. - -### Built-in functions - -The following methods (defined in the [`BasicTimePackage`](#packages) but excluded if using a [raw `Engine`]) operate on timestamps: - -| Function | Parameter(s) | Description | -| ----------------------------- | ---------------------------------- | -------------------------------------------------------- | -| `elapsed` method and property | _none_ | returns the number of seconds since the timestamp | -| `-` operator | later timestamp, earlier timestamp | returns the number of seconds between the two timestamps | - -### Examples - -```rust -let now = timestamp(); - -// Do some lengthy operation... - -if now.elapsed > 30.0 { - print("takes too long (over 30 seconds)!") -} -``` - -Comparison operators --------------------- - -Comparing most values of the same data type work out-of-the-box for all [standard types] supported by the system. - -However, if using a [raw `Engine`] without loading any [packages], comparisons can only be made between a limited -set of types (see [built-in operators](#built-in-operators)). - -```rust -42 == 42; // true -42 > 42; // false -"hello" > "foo"; // true -"42" == 42; // false -``` - -Comparing two values of _different_ data types, or of unknown data types, always results in `false`, -except for '`!=`' (not equals) which results in `true`. This is in line with intuition. - -```rust -42 == 42.0; // false - i64 cannot be compared with f64 -42 != 42.0; // true - i64 cannot be compared with f64 - -42 > "42"; // false - i64 cannot be compared with string -42 <= "42"; // false - i64 cannot be compared with string - -let ts = new_ts(); // custom type -ts == 42; // false - types cannot be compared -ts != 42; // true - types cannot be compared -``` - -Boolean operators ------------------ - -| Operator | Description | -| -------- | ------------------------------------- | -| `!` | Boolean _Not_ | -| `&&` | Boolean _And_ (short-circuits) | -| `\|\|` | Boolean _Or_ (short-circuits) | -| `&` | Boolean _And_ (doesn't short-circuit) | -| `\|` | Boolean _Or_ (doesn't short-circuit) | - -Double boolean operators `&&` and `||` _short-circuit_, meaning that the second operand will not be evaluated -if the first one already proves the condition wrong. - -Single boolean operators `&` and `|` always evaluate both operands. - -```rust -this() || that(); // that() is not evaluated if this() is true -this() && that(); // that() is not evaluated if this() is false - -this() | that(); // both this() and that() are evaluated -this() & that(); // both this() and that() are evaluated -``` - -Compound assignment operators ----------------------------- - -```rust -let number = 5; -number += 4; // number = number + 4 -number -= 3; // number = number - 3 -number *= 2; // number = number * 2 -number /= 1; // number = number / 1 -number %= 3; // number = number % 3 -number <<= 2; // number = number << 2 -number >>= 1; // number = number >> 1 -``` - -The `+=` operator can also be used to build [strings]: - -```rust -let my_str = "abc"; -my_str += "ABC"; -my_str += 12345; - -my_str == "abcABC12345" -``` - -`if` statement --------------- - -```rust -if foo(x) { - print("It's true!"); -} else if bar == baz { - print("It's true again!"); -} else if ... { - : -} else if ... { - : -} else { - print("It's finally false!"); -} -``` - -All branches of an `if` statement must be enclosed within braces '`{`' .. '`}`', even when there is only one statement. -Like Rust, there is no ambiguity regarding which `if` clause a statement belongs to. - -```rust -if (decision) print("I've decided!"); -// ^ syntax error, expecting '{' in statement block -``` - -Like Rust, `if` statements can also be used as _expressions_, replacing the `? :` conditional operators in other C-like languages. - -```rust -// The following is equivalent to C: int x = 1 + (decision ? 42 : 123) / 2; -let x = 1 + if decision { 42 } else { 123 } / 2; -x == 22; - -let x = if decision { 42 }; // no else branch defaults to '()' -x == (); -``` - -`while` loop ------------- - -```rust -let x = 10; - -while x > 0 { - x = x - 1; - if x < 6 { continue; } // skip to the next iteration - print(x); - if x == 5 { break; } // break out of while loop -} -``` - -Infinite `loop` ---------------- - -```rust -let x = 10; - -loop { - x = x - 1; - if x > 5 { continue; } // skip to the next iteration - print(x); - if x == 0 { break; } // break out of loop -} -``` - -`for` loop ----------- - -Iterating through a range or an [array] is provided by the `for` ... `in` loop. - -```rust -// Iterate through string, yielding characters -let s = "hello, world!"; - -for ch in s { - if ch > 'z' { continue; } // skip to the next iteration - print(ch); - if x == '@' { break; } // break out of for loop -} - -// Iterate through array -let array = [1, 3, 5, 7, 9, 42]; - -for x in array { - if x > 10 { continue; } // skip to the next iteration - print(x); - if x == 42 { break; } // break out of for loop -} - -// The 'range' function allows iterating from first to last-1 -for x in range(0, 50) { - if x > 10 { continue; } // skip to the next iteration - print(x); - if x == 42 { break; } // break out of for loop -} - -// The 'range' function also takes a step -for x in range(0, 50, 3) { // step by 3 - if x > 10 { continue; } // skip to the next iteration - print(x); - if x == 42 { break; } // break out of for loop -} - -// Iterate through object map -let map = #{a:1, b:3, c:5, d:7, e:9}; - -// Property names are returned in random order -for x in keys(map) { - if x > 10 { continue; } // skip to the next iteration - print(x); - if x == 42 { break; } // break out of for loop -} - -// Property values are returned in random order -for val in values(map) { - print(val); -} -``` - -`return`-ing values -------------------- - -```rust -return; // equivalent to return (); - -return 123 + 456; // returns 579 -``` - -Errors and `throw`-ing exceptions --------------------------------- - -All of [`Engine`]'s evaluation/consuming methods return `Result>` with `EvalAltResult` -holding error information. To deliberately return an error during an evaluation, use the `throw` keyword. - -```rust -if some_bad_condition_has_happened { - throw error; // 'throw' takes a string as the exception text -} - -throw; // defaults to empty exception text: "" -``` - -Exceptions thrown via `throw` in the script can be captured by matching `Err(EvalAltResult::ErrorRuntime(` _reason_ `,` _position_ `))` -with the exception text captured by the first parameter. - -```rust -let result = engine.eval::(r#" - let x = 42; - - if x > 0 { - throw x + " is too large!"; - } -"#); - -println!(result); // prints "Runtime error: 42 is too large! (line 5, position 15)" -``` - -Functions ---------- - -[function]: #functions -[functions]: #functions - -Rhai supports defining functions in script (unless disabled with [`no_function`]): - -```rust -fn add(x, y) { - return x + y; -} - -fn sub(x, y,) { // trailing comma in parameters list is OK - return x - y; -} - -print(add(2, 3)); // prints 5 -print(sub(2, 3,)); // prints -1 - trailing comma in arguments list is OK -``` - -### Implicit return - -Just like in Rust, an implicit return can be used. In fact, the last statement of a block is _always_ the block's return value -regardless of whether it is terminated with a semicolon `';'`. This is different from Rust. - -```rust -fn add(x, y) { // implicit return: - x + y; // value of the last statement (no need for ending semicolon) - // is used as the return value -} - -fn add2(x) { - return x + 2; // explicit return -} - -print(add(2, 3)); // prints 5 -print(add2(42)); // prints 44 -``` - -### No access to external scope - -Functions are not _closures_. They do not capture the calling environment and can only access their own parameters. -They cannot access variables external to the function itself. - -```rust -let x = 42; - -fn foo() { x } // <- syntax error: variable 'x' doesn't exist -``` - -### Passing arguments by value - -Functions defined in script always take [`Dynamic`] parameters (i.e. the parameter can be of any type). -It is important to remember that all arguments are passed by _value_, so all functions are _pure_ -(i.e. they never modify their arguments). -Any update to an argument will **not** be reflected back to the caller. -This can introduce subtle bugs, if not careful, especially when using the _method-call_ style. - -```rust -fn change(s) { // 's' is passed by value - s = 42; // only a COPY of 's' is changed -} - -let x = 500; -x.change(); // de-sugars to 'change(x)' -x == 500; // 'x' is NOT changed! -``` - -### Global definitions only - -Functions can only be defined at the global level, never inside a block or another function. - -```rust -// Global level is OK -fn add(x, y) { - x + y -} - -// The following will not compile -fn do_addition(x) { - fn add_y(n) { // <- syntax error: functions cannot be defined inside another function - n + y - } - - add_y(x) -} -``` - -Unlike C/C++, functions can be defined _anywhere_ within the global level. A function does not need to be defined -prior to being used in a script; a statement in the script can freely call a function defined afterwards. -This is similar to Rust and many other modern languages. - -### Function overloading - -Functions defined in script can be _overloaded_ by _arity_ (i.e. they are resolved purely upon the function's _name_ -and _number_ of parameters, but not parameter _types_ since all parameters are the same type - [`Dynamic`]). -New definitions _overwrite_ previous definitions of the same name and number of parameters. - -```rust -fn foo(x,y,z) { print("Three!!! " + x + "," + y + "," + z) } -fn foo(x) { print("One! " + x) } -fn foo(x,y) { print("Two! " + x + "," + y) } -fn foo() { print("None.") } -fn foo(x) { print("HA! NEW ONE! " + x) } // overwrites previous definition - -foo(1,2,3); // prints "Three!!! 1,2,3" -foo(42); // prints "HA! NEW ONE! 42" -foo(1,2); // prints "Two!! 1,2" -foo(); // prints "None." -``` - -Members and methods -------------------- - -Properties and methods in a Rust custom type registered with the [`Engine`] can be called just like a regular function in Rust. -Unlike functions defined in script (for which all arguments are passed by _value_), -native Rust functions may mutate the object (or the first argument if called in normal function call style). - -```rust -let a = new_ts(); // constructor function -a.field = 500; // property setter -a.update(); // method call, 'a' can be modified - -update(a); // <- this de-sugars to 'a.update()' thus if 'a' is a simple variable - // unlike scripted functions, 'a' can be modified and is not a copy - -let array = [ a ]; - -update(array[0]); // <- 'array[0]' is an expression returning a calculated value, - // a transient (i.e. a copy) so this statement has no effect - // except waste a lot of time cloning - -array[0].update(); // <- call this method-call style will update 'a' -``` - -Custom types, properties and methods can be disabled via the [`no_object`] feature. - -`print` and `debug` -------------------- - -The `print` and `debug` functions default to printing to `stdout`, with `debug` using standard debug formatting. - -```rust -print("hello"); // prints hello to stdout -print(1 + 2 + 3); // prints 6 to stdout -print("hello" + 42); // prints hello42 to stdout -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) 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 -// 'print' and 'debug' -engine.on_print(|x| println!("hello: {}", x)); -engine.on_debug(|x| println!("DEBUG: {}", x)); - -// Example: quick-'n-dirty logging -let logbook = Arc::new(RwLock::new(Vec::::new())); - -// Redirect print/debug output to 'log' -let log = logbook.clone(); -engine.on_print(move |s| log.write().unwrap().push(format!("entry: {}", s))); - -let log = logbook.clone(); -engine.on_debug(move |s| log.write().unwrap().push(format!("DEBUG: {}", s))); - -// Evaluate script -engine.eval::<()>(script)?; - -// 'logbook' captures all the 'print' and 'debug' output -for entry in logbook.read().unwrap().iter() { - println!("{}", entry); -} -``` - -Modules -------- - -[module]: #modules -[modules]: #modules - -Rhai allows organizing code (functions, both Rust-based or script-based, and variables) into _modules_. -Modules can be disabled via the [`no_module`] feature. - -### Exporting variables and functions from modules - -A _module_ is a single script (or pre-compiled `AST`) containing global variables and functions. -The `export` statement, which can only be at global level, exposes selected variables as members of a module. -Variables not exported are _private_ and invisible to the outside. -On the other hand, all functions are automatically exported, _unless_ it is explicitly opt-out with the [`private`] prefix. -Functions declared [`private`] are invisible to the outside. - -Everything exported from a module is **constant** (**read-only**). - -```rust -// This is a module script. - -fn inc(x) { x + 1 } // script-defined function - default public - -private fn foo() {} // private function - invisible to outside - -let private = 123; // variable not exported - default invisible to outside -let x = 42; // this will be exported below - -export x; // the variable 'x' is exported under its own name - -export x as answer; // the variable 'x' is exported under the alias 'answer' - // another script can load this module and access 'x' as 'module::answer' -``` - -### Importing modules - -[`import`]: #importing-modules - -A module can be _imported_ via the `import` statement, and its members are accessed via '`::`' similar to C++. - -```rust -import "crypto" as crypto; // import the script file 'crypto.rhai' as a module - -crypto::encrypt(secret); // use functions defined under the module via '::' - -crypto::hash::sha256(key); // sub-modules are also supported - -print(crypto::status); // module variables are constants - -crypto::status = "off"; // <- runtime error - cannot modify a constant -``` - -`import` statements are _scoped_, meaning that they are only accessible inside the scope that they're imported. -They can appear anywhere a normal statement can be, but in the vast majority of cases `import` statements are -group at the beginning of a script. It is not advised to deviate from this common practice unless there is -a _Very Good Reason™_. Especially, do not place an `import` statement within a loop; doing so will repeatedly -re-load the same module during every iteration of the loop! - -```rust -let mod = "crypto"; - -if secured { // new block scope - import mod as crypto; // import module (the path needs not be a constant string) - - crypto::encrypt(key); // use a function in the module -} // the module disappears at the end of the block scope - -crypto::encrypt(others); // <- this causes a run-time error because the 'crypto' module - // is no longer available! - -for x in range(0, 1000) { - import "crypto" as c; // <- importing a module inside a loop is a Very Bad Idea™ - - c.encrypt(something); -} -``` - -### Creating custom modules with Rust - -To load a custom module (written in Rust) into an [`Engine`], first create a `Module` type, add variables/functions into it, -then finally push it into a custom [`Scope`]. This has the equivalent effect of putting an `import` statement -at the beginning of any script run. - -```rust -use rhai::{Engine, Scope, Module, i64}; - -let mut engine = Engine::new(); -let mut scope = Scope::new(); - -let mut module = Module::new(); // new module -module.set_var("answer", 41_i64); // variable 'answer' under module -module.set_fn_1("inc", |x: i64| Ok(x+1)); // use the 'set_fn_XXX' API to add functions - -// Push the module into the custom scope under the name 'question' -// This is equivalent to 'import "..." as question;' -scope.push_module("question", module); - -// Use module-qualified variables -engine.eval_expression_with_scope::(&scope, "question::answer + 1")? == 42; - -// Call module-qualified functions -engine.eval_expression_with_scope::(&scope, "question::inc(question::answer)")? == 42; -``` - -### Creating a module from an `AST` - -It is easy to convert a pre-compiled `AST` into a module: just use `Module::eval_ast_as_new`. -Don't forget the `export` statement, otherwise there will be no variables exposed by the module -other than non-[`private`] functions (unless that's intentional). - -```rust -use rhai::{Engine, Module}; - -let engine = Engine::new(); - -// Compile a script into an 'AST' -let ast = engine.compile(r#" - // Functions become module functions - fn calc(x) { - x + 1 - } - fn add_len(x, y) { - x + y.len - } - - // Imported modules can become sub-modules - import "another module" as extra; - - // Variables defined at global level can become module variables - const x = 123; - let foo = 41; - let hello; - - // Variable values become constant module variable values - foo = calc(foo); - hello = "hello, " + foo + " worlds!"; - - // Finally, export the variables and modules - export - x as abc, // aliased variable name - foo, - hello, - extra as foobar; // export sub-module -"#)?; - -// Convert the 'AST' into a module, using the 'Engine' to evaluate it first -let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?; - -// 'module' now can be loaded into a custom 'Scope' for future use. It contains: -// - sub-module: 'foobar' (renamed from 'extra') -// - functions: 'calc', 'add_len' -// - variables: 'abc' (renamed from 'x'), 'foo', 'hello' -``` - -### Module resolvers - -When encountering an `import` statement, Rhai attempts to _resolve_ the module based on the path string. -_Module Resolvers_ are service types that implement the [`ModuleResolver`](#traits) trait. -There are a number of standard resolvers built into Rhai, the default being the `FileModuleResolver` -which simply loads a script file based on the path (with `.rhai` extension attached) and execute it to form a module. - -Built-in module resolvers are grouped under the `rhai::module_resolvers` module namespace. - -| Module Resolver | Description | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `FileModuleResolver` | The default module resolution service, not available under [`no_std`] or [WASM] builds. Loads a script file (based off the current directory) with `.rhai` extension.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | -| `StaticModuleResolver` | Loads modules that are statically added. This can be used under [`no_std`]. | - -An [`Engine`]'s module resolver is set via a call to `Engine::set_module_resolver`: - -```rust -// Use the 'StaticModuleResolver' -let resolver = rhai::module_resolvers::StaticModuleResolver::new(); -engine.set_module_resolver(Some(resolver)); - -// Effectively disable 'import' statements by setting module resolver to 'None' -engine.set_module_resolver(None); -``` - -Ruggedization - protect against DoS attacks ------------------------------------------- - -For scripting systems open to untrusted 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 malicious script may continuously grow a [string], an [array] or [object map] until all memory is consumed. - It may also create a large [array] or [object map] literal that exhausts all memory during parsing. -* **CPU**: A malicious script may run an infinite tight loop that consumes all CPU cycles. -* **Time**: A malicious script may run indefinitely, thereby blocking the calling system which is waiting for a result. -* **Stack**: A malicious script may attempt an infinite recursive call that exhausts the call stack. - Alternatively, it may create a degenerated deep expression with so many levels that the parser exhausts the call stack - when parsing the expression; or even deeply-nested statement blocks, if nested deep enough. -* **Overflows**: A malicious 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 malicious 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). - Furthermore, the module script may simply [`import`] itself in an infinite recursion. - Even when modules are not created from files, they still typically consume a lot of resources to load. -* **Data**: A malicious 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 length of strings - -Rhai by default does not limit how long a [string] can be. -This can be changed via the `Engine::set_max_string_size` method, with zero being unlimited (the default). - -```rust -let mut engine = Engine::new(); - -engine.set_max_string_size(500); // allow strings only up to 500 bytes long (in UTF-8 format) - -engine.set_max_string_size(0); // allow unlimited string length -``` - -A script attempting to create a string literal longer than the maximum length will terminate with a parse error. -Any script operation that produces a string longer than the maximum also terminates the script with an error result. -This check can be disabled via the [`unchecked`] feature for higher performance -(but higher risks as well). - -Be conservative when setting a maximum limit and always consider the fact that a registered function may grow -a string's length without Rhai noticing until the very end. For instance, the built-in '`+`' operator for strings -concatenates two strings together to form one longer string; if both strings are _slightly_ below the maximum -length limit, the resultant string may be almost _twice_ the maximum length. - -### Maximum size of arrays - -Rhai by default does not limit how large an [array] can be. -This can be changed via the `Engine::set_max_array_size` method, with zero being unlimited (the default). - -```rust -let mut engine = Engine::new(); - -engine.set_max_array_size(500); // allow arrays only up to 500 items - -engine.set_max_array_size(0); // allow unlimited arrays -``` - -A script attempting to create an array literal larger than the maximum will terminate with a parse error. -Any script operation that produces an array larger than the maximum also terminates the script with an error result. -This check can be disabled via the [`unchecked`] feature for higher performance -(but higher risks as well). - -Be conservative when setting a maximum limit and always consider the fact that a registered function may grow -an array's size without Rhai noticing until the very end. -For instance, the built-in '`+`' operator for arrays concatenates two arrays together to form one larger array; -if both arrays are _slightly_ below the maximum size limit, the resultant array may be almost _twice_ the maximum size. - -As a malicious script may create a deeply-nested array which consumes huge amounts of memory while each individual -array still stays under the maximum size limit, Rhai also recursively adds up the sizes of all strings, arrays -and object maps contained within each array to make sure that the _aggregate_ sizes of none of these data structures -exceed their respective maximum size limits (if any). - -### Maximum size of object maps - -Rhai by default does not limit how large (i.e. the number of properties) an [object map] can be. -This can be changed via the `Engine::set_max_map_size` method, with zero being unlimited (the default). - -```rust -let mut engine = Engine::new(); - -engine.set_max_map_size(500); // allow object maps with only up to 500 properties - -engine.set_max_map_size(0); // allow unlimited object maps -``` - -A script attempting to create an object map literal with more properties than the maximum will terminate with a parse error. -Any script operation that produces an object map with more properties than the maximum also terminates the script with an error result. -This check can be disabled via the [`unchecked`] feature for higher performance -(but higher risks as well). - -Be conservative when setting a maximum limit and always consider the fact that a registered function may grow -an object map's size without Rhai noticing until the very end. For instance, the built-in '`+`' operator for object maps -concatenates two object maps together to form one larger object map; if both object maps are _slightly_ below the maximum -size limit, the resultant object map may be almost _twice_ the maximum size. - -As a malicious script may create a deeply-nested object map which consumes huge amounts of memory while each individual -object map still stays under the maximum size limit, Rhai also recursively adds up the sizes of all strings, arrays -and object maps contained within each object map to make sure that the _aggregate_ sizes of none of these data structures -exceed their respective maximum size limits (if any). - -### 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. -To help visualize, think of an _operation_ as roughly equals to one _instruction_ of a hypothetical CPU -which includes _specialized_ instructions, such as _function call_, _load module_ etc., each taking up -one CPU cycle to execute. - -The _operations count_ is intended to be a very course-grained measurement of the amount of CPU that a script -has consumed, allowing the system to impose a hard upper limit on computing resources. - -A script exceeding the maximum operations count terminates with an error result. -This can be disabled via the [`unchecked`] feature for higher performance (but higher risks as well). - -### Tracking progress and force-terminate script run - -It is impossible to know when, or even whether, a script run will end -(a.k.a. the [Halting Problem](http://en.wikipedia.org/wiki/Halting_problem)). -When dealing with third-party untrusted scripts that may be malicious, to track 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| { // parameter is '&u64' - number of operations already performed - if count % 1000 == 0 { - println!("{}", count); // print out a progress log every 1,000 operations - } - true // return 'true' to continue running the script - // return 'false' to immediately terminate the script -}); -``` - -The closure passed to `Engine::on_progress` will be called once for every operation. -Return `false` to terminate the script immediately. - -Notice that the _operations count_ value passed into the closure does not indicate the _percentage_ of work -already done by the script (and thus it is not real _progress_ tracking), because it is impossible to determine -how long a script may run. It is possible, however, to calculate this percentage based on an estimated -total number of operations for a typical run. - -### Maximum number of modules - -Rhai by default does not limit how many [modules] can be loaded via [`import`] statements. -This can be changed via the `Engine::set_max_modules` method. Notice that setting the maximum number -of modules to zero does _not_ indicate unlimited modules, but disallows loading any module altogether. - -```rust -let mut engine = Engine::new(); - -engine.set_max_modules(5); // allow loading only up to 5 modules - -engine.set_max_modules(0); // disallow loading any module (maximum = zero) - -engine.set_max_modules(1000); // set to a large number for effectively unlimited modules -``` - -A script attempting to load more than the maximum number of modules will terminate with an error result. -This check can be disabled via the [`unchecked`] feature for higher performance -(but higher risks as well). - -### Maximum call stack depth - -Rhai by default limits function calls to a maximum depth of 128 levels (16 levels in debug build). -This limit may be changed via the `Engine::set_max_call_levels` method. - -When setting this limit, care must be also taken to the evaluation depth of each _statement_ -within the function. It is entirely possible for a malicious script to embed a recursive call deep -inside a nested expression or statement block (see [maximum statement depth](#maximum-statement-depth)). - -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. -This check can be disabled via the [`unchecked`] feature for higher performance -(but higher risks as well). - -### Maximum statement depth - -Rhai by default limits statements and expressions nesting to a maximum depth of 128 -(which should be plenty) when they are at _global_ level, but only a depth of 32 -when they are within function bodies. For debug builds, these limits are set further -downwards to 32 and 16 respectively. - -That is because it is possible to overflow the [`Engine`]'s stack when it tries to -recursively parse an extremely deeply-nested code stream. - -```rust -// The following, if long enough, can easily cause stack overflow during parsing. -let a = (1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(...)+1))))))))))); -``` - -This limit may be changed via the `Engine::set_max_expr_depths` method. There are two limits to set, -one for the maximum depth at global level, and the other for function bodies. - -```rust -let mut engine = Engine::new(); - -engine.set_max_expr_depths(50, 5); // allow nesting up to 50 layers of expressions/statements - // at global level, but only 5 inside functions -``` - -Beware that there may be multiple layers for a simple language construct, even though it may correspond -to only one AST node. That is because the Rhai _parser_ internally runs a recursive chain of function calls -and it is important that a malicious script does not panic the parser in the first place. - -Functions are placed under stricter limits because of the multiplicative effect of recursion. -A script can effectively call itself while deep inside an expression chain within the function body, -thereby overflowing the stack even when the level of recursion is within limit. - -Make sure that `C x ( 5 + F ) + S` layered calls do not cause a stack overflow, where: - -* `C` = maximum call stack depth, -* `F` = maximum statement depth for functions, -* `S` = maximum statement depth at global level. - -A script exceeding the maximum nesting depths will terminate with a parsing error. -The malicious `AST` will not be able to get past parsing in the first place. - -This check can be disabled via the [`unchecked`] feature for higher performance -(but higher risks as well). - -### Checked arithmetic - -By default, 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 -=================== - -[script optimization]: #script-optimization - -Rhai includes an _optimizer_ that tries to optimize a script after parsing. -This can reduce resource utilization and increase execution speed. -Script optimization can be turned off via the [`no_optimize`] feature. - -For example, in the following: - -```rust -{ - let x = 999; // NOT eliminated: variable may be used later on (perhaps even an 'eval') - 123; // eliminated: no effect - "hello"; // eliminated: no effect - [1, 2, x, x*2, 5]; // eliminated: no effect - foo(42); // NOT eliminated: the function 'foo' may have side-effects - 666 // NOT eliminated: this is the return value of the block, - // and the block is the last one so this is the return value of the whole script -} -``` - -Rhai attempts to eliminate _dead code_ (i.e. code that does nothing, for example an expression by itself as a statement, -which is allowed in Rhai). The above script optimizes to: - -```rust -{ - let x = 999; - foo(42); - 666 -} -``` - -Constants propagation is used to remove dead code: - -```rust -const ABC = true; -if ABC || some_work() { print("done!"); } // 'ABC' is constant so it is replaced by 'true'... -if true || some_work() { print("done!"); } // since '||' short-circuits, 'some_work' is never called -if true { print("done!"); } // <- the line above is equivalent to this -print("done!"); // <- the line above is further simplified to this - // because the condition is always true -``` - -These are quite effective for template-based machine-generated scripts where certain constant values -are spliced into the script text in order to turn on/off certain sections. -For fixed script texts, the constant values can be provided in a user-defined [`Scope`] object -to the [`Engine`] for use in compilation and evaluation. - -Beware, however, that most operators are actually function calls, and those functions can be overridden, -so they are not optimized away: - -```rust -const DECISION = 1; - -if DECISION == 1 { // NOT optimized away because you can define - : // your own '==' function to override the built-in default! - : -} else if DECISION == 2 { // same here, NOT optimized away - : -} else if DECISION == 3 { // same here, NOT optimized away - : -} else { - : -} -``` - -because no operator functions will be run (in order not to trigger side-effects) during the optimization process -(unless the optimization level is set to [`OptimizationLevel::Full`]). So, instead, do this: - -```rust -const DECISION_1 = true; -const DECISION_2 = false; -const DECISION_3 = false; - -if DECISION_1 { - : // this branch is kept and promoted to the parent level -} else if DECISION_2 { - : // this branch is eliminated -} else if DECISION_3 { - : // this branch is eliminated -} else { - : // this branch is eliminated -} -``` - -In general, boolean constants are most effective for the optimizer to automatically prune -large `if`-`else` branches because they do not depend on operators. - -Alternatively, turn the optimizer to [`OptimizationLevel::Full`]. - -Here be dragons! -================ - -Optimization levels -------------------- - -[`OptimizationLevel::Full`]: #optimization-levels -[`OptimizationLevel::Simple`]: #optimization-levels -[`OptimizationLevel::None`]: #optimization-levels - -There are actually three levels of optimizations: `None`, `Simple` and `Full`. - -* `None` is obvious - no optimization on the AST is performed. - -* `Simple` (default) performs only relatively _safe_ optimizations without causing side-effects - (i.e. it only relies on static analysis and will not actually perform any function calls). - -* `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 `Engine::set_optimization_level`: - -```rust -// Turn on aggressive optimizations -engine.set_optimization_level(rhai::OptimizationLevel::Full); -``` - -If it is ever needed to _re_-optimize an `AST`, use the `optimize_ast` method: - -```rust -// Compile script to AST -let ast = engine.compile("40 + 2")?; - -// Create a new 'Scope' - put constants in it to aid optimization if using 'OptimizationLevel::Full' -let scope = Scope::new(); - -// Re-optimize the AST -let ast = engine.optimize_ast(&scope, &ast, OptimizationLevel::Full); -``` - -When the optimization level is [`OptimizationLevel::Full`], the [`Engine`] assumes all functions to be _pure_ and will _eagerly_ -evaluated all function calls with constant arguments, using the result to replace the call. This also applies to all operators -(which are implemented as functions). For instance, the same example above: - -```rust -// When compiling the following with OptimizationLevel::Full... - -const DECISION = 1; - // this condition is now eliminated because 'DECISION == 1' -if DECISION == 1 { // is a function call to the '==' function, and it returns 'true' - print("hello!"); // this block is promoted to the parent level -} else { - print("boo!"); // this block is eliminated because it is never reached -} - -print("hello!"); // <- the above is equivalent to this - // ('print' and 'debug' are handled specially) -``` - -Because of the eager evaluation of functions, many constant expressions will be evaluated and replaced by the result. -This does not happen with [`OptimizationLevel::Simple`] which doesn't assume all functions to be _pure_. - -```rust -// When compiling the following with OptimizationLevel::Full... - -let x = (1+2)*3-4/5%6; // <- will be replaced by 'let x = 9' -let y = (1>2) || (3<=4); // <- will be replaced by 'let y = true' -``` - -Side-effect considerations --------------------------- - -All of Rhai's built-in functions (and operators which are implemented as functions) are _pure_ (i.e. they do not mutate state -nor cause any side-effects, with the exception of `print` and `debug` which are handled specially) so using -[`OptimizationLevel::Full`] is usually quite safe _unless_ custom types and functions are registered. - -If custom functions are registered, they _may_ be called (or maybe not, if the calls happen to lie within a pruned code block). -If custom functions are registered to overload built-in operators, they will also be called when the operators are used -(in an `if` statement, for example) causing side-effects. - -Therefore, the rule-of-thumb is: _always_ register custom types and functions _after_ compiling scripts if - [`OptimizationLevel::Full`] is used. _DO NOT_ depend on knowledge that the functions have no side-effects, - because those functions can change later on and, when that happens, existing scripts may break in subtle ways. - -Volatility considerations -------------------------- - -Even if a custom function does not mutate state nor cause side-effects, it may still be _volatile_, -i.e. it _depends_ on the external environment and is not _pure_. -A perfect example is a function that gets the current time - obviously each run will return a different value! -The optimizer, when using [`OptimizationLevel::Full`], will _merrily assume_ that all functions are _pure_, -so when it finds constant arguments (or none) it eagerly executes the function call and replaces it with the result. -This causes the script to behave differently from the intended semantics. - -Therefore, **avoid using [`OptimizationLevel::Full`]** if non-_pure_ custom types and/or functions are involved. - -Subtle semantic changes ------------------------ - -Some optimizations can alter subtle semantics of the script. For example: - -```rust -if true { // condition always true - 123.456; // eliminated - hello; // eliminated, EVEN THOUGH the variable doesn't exist! - foo(42) // promoted up-level -} - -foo(42) // <- the above optimizes to this -``` - -Nevertheless, if the original script were evaluated instead, it would have been an error - the variable `hello` doesn't exist, -so the script would have been terminated at that point with an error return. - -In fact, any errors inside a statement that has been eliminated will silently _disappear_: - -```rust -print("start!"); -if my_decision { /* do nothing... */ } // eliminated due to no effect -print("end!"); - -// The above optimizes to: - -print("start!"); -print("end!"); -``` - -In the script above, if `my_decision` holds anything other than a boolean value, the script should have been terminated due to -a type error. However, after optimization, the entire `if` statement is removed (because an access to `my_decision` produces -no side-effects), thus the script silently runs to completion without errors. - -Turning off optimizations -------------------------- - -It is usually a bad idea to depend on a script failing or such kind of subtleties, but if it turns out to be necessary -(why? I would never guess), turn it off by setting the optimization level to [`OptimizationLevel::None`]. - -```rust -let engine = rhai::Engine::new(); - -// Turn off the optimizer -engine.set_optimization_level(rhai::OptimizationLevel::None); -``` - -Alternatively, turn off optimizations via the [`no_optimize`] feature. - -`eval` - or "How to Shoot Yourself in the Foot even Easier" ---------------------------------------------------------- - -[`eval`]: #eval---or-how-to-shoot-yourself-in-the-foot-even-easier - -Saving the best for last: in addition to script optimizations, there is the ever-dreaded... `eval` function! - -```rust -let x = 10; - -fn foo(x) { x += 12; x } - -let script = "let y = x;"; // build a script -script += "y += foo(y);"; -script += "x + y"; - -let result = eval(script); // <- look, JS, we can also do this! - -print("Answer: " + result); // prints 42 - -print("x = " + x); // prints 10: functions call arguments are passed by value -print("y = " + y); // prints 32: variables defined in 'eval' persist! - -eval("{ let z = y }"); // to keep a variable local, use a statement block - -print("z = " + z); // <- error: variable 'z' not found - -"print(42)".eval(); // <- nope... method-call style doesn't work -``` - -Script segments passed to `eval` execute inside the current [`Scope`], so they can access and modify _everything_, -including all variables that are visible at that position in code! It is almost as if the script segments were -physically pasted in at the position of the `eval` call. But because of this, new functions cannot be defined -within an `eval` call, since functions can only be defined at the global level, not inside a function call! - -```rust -let script = "x += 32"; -let x = 10; -eval(script); // variable 'x' in the current scope is visible! -print(x); // prints 42 - -// The above is equivalent to: -let script = "x += 32"; -let x = 10; -x += 32; -print(x); -``` - -For those who subscribe to the (very sensible) motto of ["`eval` is evil"](http://linterrors.com/js/eval-is-evil), -disable `eval` by overloading it, probably with something that throws. - -```rust -fn eval(script) { throw "eval is evil! I refuse to run " + script } - -let x = eval("40 + 2"); // 'eval' here throws "eval is evil! I refuse to run 40 + 2" -``` - -Or overload it from Rust: - -```rust -fn alt_eval(script: String) -> Result<(), Box> { - Err(format!("eval is evil! I refuse to run {}", script).into()) -} - -engine.register_result_fn("eval", alt_eval); -``` - -There is even a package named [`EvalPackage`](#packages) which implements the disabling override: - -```rust -use rhai::Engine; -use rhai::packages::Package // load the 'Package' trait to use packages -use rhai::packages::EvalPackage; // the 'eval' package disables 'eval' - -let mut engine = Engine::new(); -let package = EvalPackage::new(); // create the package - -engine.load_package(package.get()); // load the package -``` +See [The Rhai Book](https://schungx.github.io/rhai/) for details on the Rhai script engine and language. diff --git a/src/lib.rs b/src/lib.rs index 868721d8..648a8dce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,7 @@ //! | ------------- | ----------------------------------------------------------------------------------------------------------------------------------| //! | `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_module` | Disable loading external modules 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. | @@ -63,7 +64,7 @@ //! | `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`. | //! -//! [Check out the README on GitHub for details on the Rhai language!](https://github.com/jonathandturner/rhai) +//! See [The Rhai Book](https://schungx.github.io/rhai/) for details on the Rhai script engine and language. #![cfg_attr(feature = "no_std", no_std)]