Complete built-in operators.

This commit is contained in:
Stephen Chung 2020-05-24 00:29:06 +08:00
parent b49e1e199a
commit d56634cac7
7 changed files with 131 additions and 122 deletions

View File

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

View File

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

View File

@ -16,6 +16,8 @@ Breaking changes
* The `RegisterDynamicFn` trait is merged into the `RegisterResutlFn` trait which now always returns
`Result<Dynamic, Box<EvalAltResult>>`.
* 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
==============

View File

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

View File

@ -710,8 +710,7 @@ fn parse_call_expr<'a>(
input: &mut Peekable<TokenIterator<'a>>,
state: &mut ParseState,
id: String,
#[cfg(not(feature = "no_module"))] mut modules: Option<Box<ModuleRef>>,
#[cfg(feature = "no_module")] modules: Option<ModuleRef>,
mut modules: Option<Box<ModuleRef>>,
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),

View File

@ -80,63 +80,66 @@ fn test_module_resolver() -> Result<(), Box<EvalAltResult>> {
42
);
engine.set_max_modules(5);
#[cfg(not(feature = "unchecked"))]
{
engine.set_max_modules(5);
assert!(matches!(
*engine
.eval::<INT>(
r#"
let sum = 0;
assert!(matches!(
*engine
.eval::<INT>(
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::<INT>(
r#"
let sum = 0;
#[cfg(not(feature = "no_function"))]
assert!(matches!(
*engine
.eval::<INT>(
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(())
}

View File

@ -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<EvalAltResult>> {
let engine = Engine::new();
@ -73,6 +74,7 @@ fn test_stack_overflow_parsing() -> Result<(), Box<EvalAltResult>> {
err if err.error_type() == &ParseErrorType::ExprTooDeep
));
#[cfg(not(feature = "no_function"))]
engine.compile("fn abc(x) { x + 1 }")?;
Ok(())