diff --git a/Cargo.toml b/Cargo.toml index 981f1241..6239e658 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,26 +20,22 @@ categories = [ "no-std", "embedded", "parser-implementations" ] num-traits = { version = "0.2.11", default-features = false } [features] -#default = ["no_stdlib", "no_function", "no_index", "no_object", "no_module", "no_float", "only_i32", "unchecked", "no_optimize", "sync"] +#default = ["unchecked", "sync", "no_optimize", "no_float", "only_i32", "no_index", "no_object", "no_function", "no_module"] default = [] unchecked = [] # unchecked arithmetic -no_index = [] # no arrays and indexing -no_float = [] # no floating-point -no_function = [] # no script-defined functions -no_object = [] # no custom objects +sync = [] # restrict to only types that implement Send + Sync no_optimize = [] # no script optimizer -no_module = [] # no modules +no_float = [] # no floating-point only_i32 = [] # set INT=i32 (useful for 32-bit systems) only_i64 = [] # set INT=i64 (default) and disable support for all other integer types -sync = [] # restrict to only types that implement Send + Sync +no_index = [] # no arrays and indexing +no_object = [] # no custom objects +no_function = [] # no script-defined functions +no_module = [] # no modules # compiling for no-std no_std = [ "num-traits/libm", "hashbrown", "core-error", "libm", "ahash" ] -# other developer features -no_stdlib = [] # do not register the standard library -optimize_full = [] # set optimization level to Full (default is Simple) - this is a feature used only to simplify testing - [profile.release] lto = "fat" codegen-units = 1 diff --git a/README.md b/README.md index fc04a54e..a2f13f3a 100644 --- a/README.md +++ b/README.md @@ -68,19 +68,19 @@ Beware that in order to use pre-releases (e.g. alpha and beta), the exact versio Optional features ----------------- -| Feature | Description | -| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `unchecked` | Exclude arithmetic checking (such as over-flows and division by zero), stack depth limit and operations count limit. Beware that a bad script may panic the entire system! | -| `no_function` | Disable script-defined functions. | -| `no_index` | Disable [arrays] and indexing features. | -| `no_object` | Disable support for custom types and object maps. | -| `no_float` | Disable floating-point numbers and math. | -| `no_optimize` | Disable the script optimizer. | -| `no_module` | Disable modules. | -| `only_i32` | Set the system integer type to `i32` and disable all other integer types. `INT` is set to `i32`. | -| `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. | -| `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | -| `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, all Rhai types, including [`Engine`], [`Scope`] and `AST`, are all `Send + Sync`. | +| 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 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. @@ -88,16 +88,16 @@ Excluding unneeded functionalities can result in smaller, faster builds as well as more control over what a script can (or cannot) do. [`unchecked`]: #optional-features -[`no_index`]: #optional-features -[`no_float`]: #optional-features -[`no_function`]: #optional-features -[`no_object`]: #optional-features +[`sync`]: #optional-features [`no_optimize`]: #optional-features -[`no_module`]: #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 -[`sync`]: #optional-features ### Performance builds @@ -133,9 +133,9 @@ Omitting arrays (`no_index`) yields the most code-size savings, followed by floa (`no_float`), checked arithmetic (`unchecked`) and finally object maps and custom types (`no_object`). 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 which does not register _any_ utility functions. -This makes the scripting language quite useless as even basic arithmetic operators are not supported. -Selectively include the necessary functionalities by loading specific [packages] to minimize the footprint. +[`Engine::new_raw`](#raw-engine) creates a _raw_ engine. +A _raw_ engine supports, out of the box, only a very restricted set 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. Related @@ -376,7 +376,8 @@ 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 _nothing_ is added, not even basic arithmetic and logic operators! +Use `Engine::new_raw` to create a _raw_ `Engine`, in which only a minimal set of basic arithmetic and logical operators +are supported. ### Packages @@ -400,20 +401,20 @@ engine.load_package(package.get()); // load the package manually. 'g The follow packages are available: -| Package | Description | In `CorePackage` | In `StandardPackage` | -| ---------------------- | ----------------------------------------------- | :--------------: | :------------------: | -| `ArithmeticPackage` | Arithmetic operators (e.g. `+`, `-`, `*`, `/`) | Yes | Yes | -| `BasicIteratorPackage` | Numeric ranges (e.g. `range(1, 10)`) | Yes | Yes | -| `LogicPackage` | Logic and comparison operators (e.g. `==`, `>`) | Yes | Yes | -| `BasicStringPackage` | Basic string functions | Yes | Yes | -| `BasicTimePackage` | Basic time functions (e.g. [timestamps]) | Yes | Yes | -| `MoreStringPackage` | Additional string functions | No | Yes | -| `BasicMathPackage` | Basic math functions (e.g. `sin`, `sqrt`) | No | Yes | -| `BasicArrayPackage` | Basic [array] functions | No | Yes | -| `BasicMapPackage` | Basic [object map] functions | No | Yes | -| `EvalPackage` | Disable [`eval`] | No | No | -| `CorePackage` | Basic essentials | | | -| `StandardPackage` | Standard library | | | +| Package | Description | In `CorePackage` | In `StandardPackage` | +| ---------------------- | -------------------------------------------------------------------------- | :--------------: | :------------------: | +| `ArithmeticPackage` | Arithmetic operators (e.g. `+`, `-`, `*`, `/`) for different numeric types | Yes | Yes | +| `BasicIteratorPackage` | Numeric ranges (e.g. `range(1, 10)`) | Yes | Yes | +| `LogicPackage` | Logical and comparison operators (e.g. `==`, `>`) | Yes | Yes | +| `BasicStringPackage` | Basic string functions | Yes | Yes | +| `BasicTimePackage` | Basic time functions (e.g. [timestamps]) | Yes | Yes | +| `MoreStringPackage` | Additional string functions | No | Yes | +| `BasicMathPackage` | Basic math functions (e.g. `sin`, `sqrt`) | No | Yes | +| `BasicArrayPackage` | Basic [array] functions | No | Yes | +| `BasicMapPackage` | Basic [object map] functions | No | Yes | +| `EvalPackage` | Disable [`eval`] | No | No | +| `CorePackage` | Basic essentials | | | +| `StandardPackage` | Standard library | | | 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). @@ -795,7 +796,7 @@ let result: i64 = engine.eval("1 + 1.0"); // prints 2.0 (normally an e ``` Use operator overloading for custom types (described below) only. -Be very careful when overloading built-in operators because script writers expect standard operators to behave in a +Be very careful when overloading 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`]. @@ -2345,6 +2346,10 @@ engine.set_max_modules(5); // allow loading only up to 5 module engine.set_max_modules(0); // allow 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). @@ -2366,6 +2371,8 @@ engine.set_max_call_levels(0); // allow no function calls at all (m ``` 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 @@ -2409,15 +2416,17 @@ Make sure that `C x ( 5 + F ) + S` layered calls do not cause a stack overflow, A script exceeding the maximum nesting depths will terminate with a parsing error. The malignant `AST` will not be able to get past parsing in the first place. -The limits can be disabled via the [`unchecked`] feature for higher performance +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). +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 @@ -2433,7 +2442,6 @@ engine.register_get("add", add); // configure 'engine' let engine = engine; // shadow the variable so that 'engine' is now immutable ``` - Script optimization =================== diff --git a/RELEASES.md b/RELEASES.md index 87df6bab..42d2f77d 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -16,6 +16,8 @@ Breaking changes * The `RegisterDynamicFn` trait is merged into the `RegisterResutlFn` trait which now always returns `Result>`. * Default maximum limit on levels of nested function calls is fine-tuned and set to a different value. +* Some operator functions are now built in (see _Speed enhancements_ below), so they are available even + when under `Engine::new_raw`. New features ------------ @@ -24,6 +26,15 @@ New features * New `EvalPackage` to disable `eval`. * More benchmarks. +Speed enhancements +------------------ + +* Common operators (e.g. `+`, `>`, `==`) now call into highly efficient built-in implementations for standard types + (i.e. `INT`, `FLOAT`, `bool`, `char`, `()` and some `String`) if not overridden by a registered function. + This yields a 5-10% speed benefit depending on script operator usage. +* Implementations of common operators for standard types are removed from the `ArithmeticPackage` and `LogicPackage` + (and therefore the `CorePackage`) because they are now always available, even under `Engine::new_raw`. + Version 0.14.1 ============== diff --git a/src/engine.rs b/src/engine.rs index 18878a65..0e9b41f7 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -392,13 +392,8 @@ impl Default for Engine { optimization_level: OptimizationLevel::None, #[cfg(not(feature = "no_optimize"))] - #[cfg(not(feature = "optimize_full"))] optimization_level: OptimizationLevel::Simple, - #[cfg(not(feature = "no_optimize"))] - #[cfg(feature = "optimize_full")] - optimization_level: OptimizationLevel::Full, - max_call_stack_depth: MAX_CALL_STACK_DEPTH, max_expr_depth: MAX_EXPR_DEPTH, max_function_expr_depth: MAX_FUNCTION_EXPR_DEPTH, @@ -406,10 +401,6 @@ impl Default for Engine { max_modules: u64::MAX, }; - #[cfg(feature = "no_stdlib")] - engine.load_package(CorePackage::new().get()); - - #[cfg(not(feature = "no_stdlib"))] engine.load_package(StandardPackage::new().get()); engine @@ -534,13 +525,8 @@ impl Engine { optimization_level: OptimizationLevel::None, #[cfg(not(feature = "no_optimize"))] - #[cfg(not(feature = "optimize_full"))] optimization_level: OptimizationLevel::Simple, - #[cfg(not(feature = "no_optimize"))] - #[cfg(feature = "optimize_full")] - optimization_level: OptimizationLevel::Full, - max_call_stack_depth: MAX_CALL_STACK_DEPTH, max_expr_depth: MAX_EXPR_DEPTH, max_function_expr_depth: MAX_FUNCTION_EXPR_DEPTH, @@ -635,13 +621,15 @@ impl Engine { ) -> Result<(Dynamic, bool), Box> { self.inc_operations(state, pos)?; + let native_only = hashes.1 == 0; + // Check for stack overflow if level > self.max_call_stack_depth { return Err(Box::new(EvalAltResult::ErrorStackOverflow(pos))); } // First search in script-defined functions (can override built-in) - if hashes.1 > 0 { + if !native_only { if let Some(fn_def) = state.get_function(hashes.1) { let (result, state2) = self.call_script_fn(scope, *state, fn_name, fn_def, args, pos, level)?; @@ -710,8 +698,8 @@ impl Engine { } // If it is a 2-operand operator, see if it is built in - if args.len() == 2 && args[0].type_id() == args[1].type_id() { - match run_builtin_op(fn_name, args[0], args[1])? { + if native_only && args.len() == 2 && args[0].type_id() == args[1].type_id() { + match run_builtin_binary_op(fn_name, args[0], args[1])? { Some(v) => return Ok((v, false)), None => (), } @@ -1949,8 +1937,8 @@ impl Engine { } } -/// Build in certain common operator implementations to avoid the cost of searching through the functions space. -fn run_builtin_op( +/// Build in common binary operator implementations to avoid the cost of calling a registered function. +fn run_builtin_binary_op( op: &str, x: &Dynamic, y: &Dynamic, diff --git a/src/parser.rs b/src/parser.rs index 928a81ff..a1d86fc0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -710,8 +710,7 @@ fn parse_call_expr<'a>( input: &mut Peekable>, state: &mut ParseState, id: String, - #[cfg(not(feature = "no_module"))] mut modules: Option>, - #[cfg(feature = "no_module")] modules: Option, + mut modules: Option>, begin: Position, level: usize, allow_stmt_expr: bool, @@ -753,12 +752,13 @@ fn parse_call_expr<'a>( let qualifiers = modules.iter().map(|(m, _)| m.as_str()); calc_fn_hash(qualifiers, &id, 0, empty()) } else { + // Qualifiers (none) + function name + no parameters. calc_fn_hash(empty(), &id, 0, empty()) } }; // Qualifiers (none) + function name + no parameters. #[cfg(feature = "no_module")] - let hash_fn_def = calc_fn_hash(empty(), &id, empty()); + let hash_fn_def = calc_fn_hash(empty(), &id, 0, empty()); return Ok(Expr::FnCall(Box::new(( (id.into(), false, begin), @@ -794,12 +794,13 @@ fn parse_call_expr<'a>( let qualifiers = modules.iter().map(|(m, _)| m.as_str()); calc_fn_hash(qualifiers, &id, args.len(), empty()) } else { + // Qualifiers (none) + function name + number of arguments. calc_fn_hash(empty(), &id, args.len(), empty()) } }; - // Qualifiers (none) + function name + dummy parameter types (one for each parameter). + // Qualifiers (none) + function name + number of arguments. #[cfg(feature = "no_module")] - let hash_fn_def = calc_fn_hash(empty(), &id, args_iter); + let hash_fn_def = calc_fn_hash(empty(), &id, args.len(), empty()); return Ok(Expr::FnCall(Box::new(( (id.into(), false, begin), diff --git a/tests/modules.rs b/tests/modules.rs index 4e475d1c..6e49a43c 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -80,63 +80,66 @@ fn test_module_resolver() -> Result<(), Box> { 42 ); - engine.set_max_modules(5); + #[cfg(not(feature = "unchecked"))] + { + engine.set_max_modules(5); - assert!(matches!( - *engine - .eval::( - r#" - let sum = 0; + assert!(matches!( + *engine + .eval::( + r#" + let sum = 0; - for x in range(0, 10) { - import "hello" as h; - sum += h::answer; - } + for x in range(0, 10) { + import "hello" as h; + sum += h::answer; + } - sum + sum "# - ) - .expect_err("should error"), - EvalAltResult::ErrorTooManyModules(_) - )); + ) + .expect_err("should error"), + EvalAltResult::ErrorTooManyModules(_) + )); - #[cfg(not(feature = "no_function"))] - assert!(matches!( - *engine - .eval::( - r#" - let sum = 0; + #[cfg(not(feature = "no_function"))] + assert!(matches!( + *engine + .eval::( + r#" + let sum = 0; - fn foo() { - import "hello" as h; - sum += h::answer; - } + fn foo() { + import "hello" as h; + sum += h::answer; + } - for x in range(0, 10) { - foo(); - } + for x in range(0, 10) { + foo(); + } - sum + sum "# - ) - .expect_err("should error"), - EvalAltResult::ErrorInFunctionCall(fn_name, _, _) if fn_name == "foo" - )); + ) + .expect_err("should error"), + EvalAltResult::ErrorInFunctionCall(fn_name, _, _) if fn_name == "foo" + )); - engine.set_max_modules(0); + engine.set_max_modules(0); - #[cfg(not(feature = "no_function"))] - engine.eval::<()>( - r#" - fn foo() { - import "hello" as h; - } + #[cfg(not(feature = "no_function"))] + engine.eval::<()>( + r#" + fn foo() { + import "hello" as h; + } - for x in range(0, 10) { - foo(); - } + for x in range(0, 10) { + foo(); + } "#, - )?; + )?; + } Ok(()) } diff --git a/tests/stack.rs b/tests/stack.rs index c0cb8a38..1ba72e6a 100644 --- a/tests/stack.rs +++ b/tests/stack.rs @@ -1,7 +1,8 @@ -#![cfg(not(feature = "no_function"))] +#![cfg(not(feature = "unchecked"))] use rhai::{Engine, EvalAltResult, ParseErrorType}; #[test] +#[cfg(not(feature = "no_function"))] fn test_stack_overflow_fn_calls() -> Result<(), Box> { let engine = Engine::new(); @@ -73,6 +74,7 @@ fn test_stack_overflow_parsing() -> Result<(), Box> { err if err.error_type() == &ParseErrorType::ExprTooDeep )); + #[cfg(not(feature = "no_function"))] engine.compile("fn abc(x) { x + 1 }")?; Ok(())