Merge branch 'master' into plugins

This commit is contained in:
Stephen Chung 2020-07-17 11:44:23 +08:00
commit a7f564fe37
42 changed files with 561 additions and 172 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "rhai" name = "rhai"
version = "0.17.0" version = "0.18.0"
edition = "2018" edition = "2018"
authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung"] authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung"]
description = "Embedded scripting for Rust" description = "Embedded scripting for Rust"
@ -73,9 +73,3 @@ optional = true
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
instant= { version = "0.1.4", features = ["wasm-bindgen"] } # WASM implementation of std::time::Instant instant= { version = "0.1.4", features = ["wasm-bindgen"] } # WASM implementation of std::time::Instant
[package.metadata.docs.rs]
features = ["serde"]
[package.metadata.playground]
features = ["serde"]

View File

@ -18,8 +18,8 @@ Supported targets and builds
* WebAssembly (WASM) * WebAssembly (WASM)
* `no-std` * `no-std`
Features Standard features
-------- -----------------
* Easy-to-use language similar to JavaScript+Rust with dynamic typing. * Easy-to-use language similar to JavaScript+Rust with dynamic typing.
* Tight integration with native Rust [functions](https://schungx.github.io/rhai/rust/functions.html) and [types]([#custom-types-and-methods](https://schungx.github.io/rhai/rust/custom.html)), including [getters/setters](https://schungx.github.io/rhai/rust/getters-setters.html), [methods](https://schungx.github.io/rhai/rust/custom.html) and [indexers](https://schungx.github.io/rhai/rust/indexers.html). * Tight integration with native Rust [functions](https://schungx.github.io/rhai/rust/functions.html) and [types]([#custom-types-and-methods](https://schungx.github.io/rhai/rust/custom.html)), including [getters/setters](https://schungx.github.io/rhai/rust/getters-setters.html), [methods](https://schungx.github.io/rhai/rust/custom.html) and [indexers](https://schungx.github.io/rhai/rust/indexers.html).
@ -30,20 +30,30 @@ Features
* Relatively little `unsafe` code (yes there are some for performance reasons, and most `unsafe` code is limited to * 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_"`). one single source file, all with names starting with `"unsafe_"`).
* Re-entrant scripting engine can be made `Send + Sync` (via the `sync` feature). * 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 - protected against malicious attacks (such as [stack-overflow](https://schungx.github.io/rhai/safety/max-call-stack.html), [over-sized data](https://schungx.github.io/rhai/safety/max-string-size.html), and [runaway scripts](https://schungx.github.io/rhai/safety/max-operations.html) etc.) that may come from untrusted third-party user-land scripts.
* Track script evaluation [progress](https://schungx.github.io/rhai/safety/progress.html) and manually terminate a script run.
* [Function overloading](https://schungx.github.io/rhai/language/overload.html). * [Function overloading](https://schungx.github.io/rhai/language/overload.html).
* [Operator overloading](https://schungx.github.io/rhai/rust/operators.html). * [Operator overloading](https://schungx.github.io/rhai/rust/operators.html).
* Support for use as a [DSL](https://schungx.github.io/rhai/engine/dsl.html) - [disabling keywords/operators](https://schungx.github.io/rhai/engine/disable.html), [custom operators](https://schungx.github.io/rhai/engine/custom-op.html) and extending the language with [custom syntax](https://schungx.github.io/rhai/engine/custom-syntax.html).
* Dynamic dispatch via [function pointers](https://schungx.github.io/rhai/language/fn-ptr.html). * Dynamic dispatch via [function pointers](https://schungx.github.io/rhai/language/fn-ptr.html).
* Some support for [object-oriented programming (OOP)](https://schungx.github.io/rhai/language/oop.html). * Some support for [object-oriented programming (OOP)](https://schungx.github.io/rhai/language/oop.html).
* Organize code base with dynamically-loadable [modules](https://schungx.github.io/rhai/language/modules.html). * Organize code base with dynamically-loadable [modules](https://schungx.github.io/rhai/language/modules.html).
* Serialization/deserialization support via [serde](https://crates.io/crates/serde) (requires the `serde` feature). * Serialization/deserialization support via [serde](https://crates.io/crates/serde) (requires the `serde` feature).
* Surgically disable keywords and operators to restrict the language.
* Scripts are [optimized](https://schungx.github.io/rhai/engine/optimize.html) (useful for template-based machine-generated scripts) for repeated evaluations. * Scripts are [optimized](https://schungx.github.io/rhai/engine/optimize.html) (useful for template-based machine-generated scripts) for repeated evaluations.
* Support for [minimal builds](https://schungx.github.io/rhai/start/builds/minimal.html) by excluding unneeded language [features](https://schungx.github.io/rhai/start/features.html). * Support for [minimal builds](https://schungx.github.io/rhai/start/builds/minimal.html) by excluding unneeded language [features](https://schungx.github.io/rhai/start/features.html).
Protection against attacks
--------------------------
* Sand-boxed - the scripting engine, if declared immutable, cannot mutate the containing environment unless explicitly permitted (e.g. via a `RefCell`).
* Rugged - protected against malicious attacks (such as [stack-overflow](https://schungx.github.io/rhai/safety/max-call-stack.html), [over-sized data](https://schungx.github.io/rhai/safety/max-string-size.html), and [runaway scripts](https://schungx.github.io/rhai/safety/max-operations.html) etc.) that may come from untrusted third-party user-land scripts.
* Track script evaluation [progress](https://schungx.github.io/rhai/safety/progress.html) and manually terminate a script run.
For those who actually want their own language
---------------------------------------------
* Use as a [DSL](https://schungx.github.io/rhai/engine/dsl.html).
* Restrict the language by surgically [disabling keywords and operators](https://schungx.github.io/rhai/engine/disable.html).
* Define [custom operators](https://schungx.github.io/rhai/engine/custom-op.html).
* Extend the language with [custom syntax](https://schungx.github.io/rhai/engine/custom-syntax.html).
Documentation Documentation
------------- -------------

View File

@ -1,15 +1,27 @@
Rhai Release Notes Rhai Release Notes
================== ==================
Version 0.18.0
==============
New features
------------
* `call` can now be called function-call style for function pointers - this is to handle builds with `no_object`.
* Disallow many keywords as variables, such as `print`, `eval`, `call`, `this` etc.
* `x.call(f, ...)` allows binding `x` to `this` for the function referenced by the function pointer `f`.
Version 0.17.0 Version 0.17.0
============== ==============
This version adds: This version adds:
* [`serde`](https://crates.io/crates/serde) support for working with `Dynamic` values (particularly _object maps_). * [`serde`](https://crates.io/crates/serde) support for working with `Dynamic` values (particularly _object maps_).
* Ability to surgically disable keywords and/or operators in the language.
* Ability to define custom operators (which must be valid identifiers).
* Low-level API to register functions. * Low-level API to register functions.
* Surgically disable keywords and/or operators in the language.
* Define custom operators.
* Extend the language via custom syntax.
Bug fixes Bug fixes
--------- ---------
@ -22,7 +34,6 @@ Breaking changes
* `EvalAltResult::ErrorMismatchOutputType` has an extra argument containing the name of the requested type. * `EvalAltResult::ErrorMismatchOutputType` has an extra argument containing the name of the requested type.
* `Engine::call_fn_dynamic` take an extra argument, allowing a `Dynamic` value to be bound to the `this` pointer. * `Engine::call_fn_dynamic` take an extra argument, allowing a `Dynamic` value to be bound to the `this` pointer.
* Precedence of the `%` (modulo) operator is lowered to below `<<` ad `>>`. This is to handle the case of `x << 3 % 10`. * Precedence of the `%` (modulo) operator is lowered to below `<<` ad `>>`. This is to handle the case of `x << 3 % 10`.
* Many configuration/setting API's now returns `&mut Self` so that the calls can be chained. This should not affect most code.
New features New features
------------ ------------
@ -31,6 +42,7 @@ New features
This is particularly useful when converting a Rust `struct` to a `Dynamic` _object map_ and back. This is particularly useful when converting a Rust `struct` to a `Dynamic` _object map_ and back.
* `Engine::disable_symbol` to surgically disable keywords and/or operators. * `Engine::disable_symbol` to surgically disable keywords and/or operators.
* `Engine::register_custom_operator` to define a custom operator. * `Engine::register_custom_operator` to define a custom operator.
* `Engine::register_custom_syntax` to define a custom syntax.
* New low-level API `Engine::register_raw_fn` and `Engine::register_raw_fn_XXX`. * New low-level API `Engine::register_raw_fn` and `Engine::register_raw_fn_XXX`.
* New low-level API `Module::set_raw_fn` mirroring `Engine::register_raw_fn`. * New low-level API `Module::set_raw_fn` mirroring `Engine::register_raw_fn`.
* `AST::clone_functions_only`, `AST::clone_functions_only_filtered` and `AST::clone_statements_only` to clone only part of an `AST`. * `AST::clone_functions_only`, `AST::clone_functions_only_filtered` and `AST::clone_statements_only` to clone only part of an `AST`.
@ -38,6 +50,8 @@ New features
* `FnPtr` is exposed as the function pointer type. * `FnPtr` is exposed as the function pointer type.
* `rhai::module_resolvers::ModuleResolversCollection` added to try a list of module resolvers. * `rhai::module_resolvers::ModuleResolversCollection` added to try a list of module resolvers.
* It is now possible to mutate the first argument of a module-qualified function call when the argument is a simple variable (but not a module constant). * It is now possible to mutate the first argument of a module-qualified function call when the argument is a simple variable (but not a module constant).
* Many configuration/setting API's now returns `&mut Self` so that the calls can be chained.
* `String` parameters in functions are supported (but inefficiently).
Version 0.16.1 Version 0.16.1

View File

@ -5,6 +5,12 @@ Advanced Topics
This section covers advanced features such as: This section covers advanced features such as:
* [Script optimization] * Simulated [Object Oriented Programming][OOP].
* The dreaded (or beloved for those with twisted tastes) [`eval`] statement * [`serde`] integration.
* [Script optimization].
* [Domain-Specific Languages][DSL].
* The dreaded (or beloved for those with twisted tastes) [`eval`] statement.

View File

@ -1,5 +1,5 @@
{ {
"version": "0.17.0", "version": "0.18.0",
"rootUrl": "", "rootUrl": "",
"rootUrlX": "/rhai", "rootUrlX": "/rhai",
"rootUrlXX": "/rhai/vnext" "rootUrlXX": "/rhai/vnext"

View File

@ -91,5 +91,5 @@ let result = engine.call_fn_dynamic(
[ 41_i64.into() ] [ 41_i64.into() ]
)?; )?;
assert_eq!(value.as_int().unwrap(), 42); assert_eq!(value.as_int()?, 42);
``` ```

View File

@ -21,7 +21,7 @@ let mut engine = Engine::new();
// (i.e. between +|- and *|/) // (i.e. between +|- and *|/)
// Also register the implementation of the customer operator as a function // Also register the implementation of the customer operator as a function
engine engine
.register_custom_operator("foo", 160).unwrap() .register_custom_operator("foo", 160)?
.register_fn("foo", |x: i64, y: i64| (x * y) - (x + y)); .register_fn("foo", |x: i64, y: i64| (x * y) - (x + y));
// The custom operator can be used in expressions // The custom operator can be used in expressions
@ -72,7 +72,7 @@ _Unary_ custom operators are not supported.
```rust ```rust
engine engine
.register_custom_operator("foo", 160).unwrap() .register_custom_operator("foo", 160)?
.register_fn("foo", |x: i64| x * x); .register_fn("foo", |x: i64| x * x);
engine.eval::<i64>("1 + 2 * 3 foo 4 - 5 / 6")?; // error: function 'foo (i64, i64)' not found engine.eval::<i64>("1 + 2 * 3 foo 4 - 5 / 6")?; // error: function 'foo (i64, i64)' not found

View File

@ -66,7 +66,7 @@ item.is::<i64>() == true; // 'is' returns whether a 'Dynam
let value = item.cast::<i64>(); // if the element is 'i64', this succeeds; otherwise it panics let value = item.cast::<i64>(); // if the element is 'i64', this succeeds; otherwise it panics
let value: i64 = item.cast(); // type can also be inferred let value: i64 = item.cast(); // type can also be inferred
let value = item.try_cast::<i64>().unwrap(); // 'try_cast' does not panic when the cast fails, but returns 'None' let value = item.try_cast::<i64>()?; // 'try_cast' does not panic when the cast fails, but returns 'None'
``` ```
Type Name Type Name

View File

@ -6,7 +6,7 @@ Function Pointers
It is possible to store a _function pointer_ in a variable just like a normal value. It is possible to store a _function pointer_ in a variable just like a normal value.
In fact, internally a function pointer simply stores the _name_ of the function as a string. In fact, internally a function pointer simply stores the _name_ of the function as a string.
Call a function pointer using the `call` method, which needs to be called in method-call style. Call a function pointer using the `call` method.
Built-in methods Built-in methods
@ -40,7 +40,7 @@ func.call(1) == 42; // call a function pointer with the 'call' method
foo(1) == 42; // <- the above de-sugars to this foo(1) == 42; // <- the above de-sugars to this
call(func, 1); //<- error: 'call (Fn, i64)' is not a registered function call(func, 1); // normal function call style also works for 'call'
let len = Fn("len"); // 'Fn' also works with registered native Rust functions let len = Fn("len"); // 'Fn' also works with registered native Rust functions
@ -66,20 +66,18 @@ Because of their dynamic nature, function pointers cannot refer to functions in
See [function namespaces] for more details. See [function namespaces] for more details.
```rust ```rust
import "foo" as f; // assume there is 'f::do_something()' import "foo" as f; // assume there is 'f::do_work()'
f::do_something(); // works! f::do_work(); // works!
let p = Fn("f::do_something"); let p = Fn("f::do_work"); // error: invalid function name
p.call(); // error: function not found - 'f::do_something' fn do_work_now() { // call it from a local function
fn do_something_now() { // call it from a local function
import "foo" as f; import "foo" as f;
f::do_something(); f::do_work();
} }
let p = Fn("do_something_now"); let p = Fn("do_work_now");
p.call(); // works! p.call(); // works!
``` ```
@ -134,3 +132,35 @@ let func = sign(x) + 1;
// Dynamic dispatch // Dynamic dispatch
map[func].call(42); map[func].call(42);
``` ```
Binding the `this` Pointer
-------------------------
When `call` is called as a _method_ but not on a `FnPtr` value, it is possible to dynamically dispatch
to a function call while binding the object in the method call to the `this` pointer of the function.
To achieve this, pass the `FnPtr` value as the _first_ argument to `call`:
```rust
fn add(x) { this += x; } // define function which uses 'this'
let func = Fn("add"); // function pointer to 'add'
func.call(1); // error: 'this' pointer is not bound
let x = 41;
func.call(x, 1); // error: function 'add (i64, i64)' not found
call(func, x, 1); // error: function 'add (i64, i64)' not found
x.call(func, 1); // 'this' is bound to 'x', dispatched to 'func'
x == 42;
```
Beware that this only works for _method-call_ style. Normal function-call style cannot bind
the `this` pointer (for syntactic reasons).
Therefore, obviously, binding the `this` pointer is unsupported under [`no_function`].

View File

@ -3,6 +3,10 @@ Call Method as Function
{{#include ../links.md}} {{#include ../links.md}}
First `&mut` Reference Parameter
-------------------------------
Property [getters/setters] and [methods][custom types] in a Rust custom type registered with the [`Engine`] can be called Property [getters/setters] and [methods][custom types] in a Rust custom type registered with the [`Engine`] can be called
just like a regular function. In fact, like Rust, property getters/setters and object methods just like a regular function. In fact, like Rust, property getters/setters and object methods
are registered as regular [functions] in Rhai that take a first `&mut` parameter. are registered as regular [functions] in Rhai that take a first `&mut` parameter.
@ -31,3 +35,23 @@ update(array[0]); // <- 'array[0]' is an expression returning a calculated val
array[0].update(); // <- call in method-call style will update 'a' array[0].update(); // <- call in method-call style will update 'a'
``` ```
Encouraged Usage
----------------
Using a `&mut` first parameter is highly encouraged when using types that are expensive to clone,
even when the intention is not to mutate that argument, because it avoids cloning that argument value.
For primary types that are cheap to clone, including `ImmutableString`, this is not necessary.
Avoid `&mut ImmutableString`
---------------------------
`ImmutableString`, Rhai internal [string] type, is an exception.
`ImmutableString` is cheap to clone, but expensive to take a mutable reference (because the underlying
string must be cloned to make a private copy).
Therefore, avoid using `&mut ImmutableString` unless the intention is to mutate it.

View File

@ -6,20 +6,25 @@ Strings and Characters
String in Rhai contain any text sequence of valid Unicode characters. String in Rhai contain any text sequence of valid Unicode characters.
Internally strings are stored in UTF-8 encoding. Internally strings are stored in UTF-8 encoding.
Strings can be built up from other strings and types via the `+` operator (provided by the [`MoreStringPackage`][packages] Strings can be built up from other strings and types via the `+` operator
but excluded if using a [raw `Engine`]). This is particularly useful when printing output. (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"`. [`type_of()`] a string returns `"string"`.
The maximum allowed length of a string can be controlled via `Engine::set_max_string_size` The maximum allowed length of a string can be controlled via `Engine::set_max_string_size`
(see [maximum length of strings]). (see [maximum length of strings]).
The `ImmutableString` Type The `ImmutableString` Type
------------------------- -------------------------
All strings in Rhai are implemented as `ImmutableString` (see [standard types]). 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. `ImmutableString` should be used in place of the standard Rust type `String` when registering functions
because using `String` is very inefficient (the `String` must always be cloned).
A alternative is to use `&str` which maps straight to `ImmutableString`.
String and Character Literals String and Character Literals
@ -59,13 +64,13 @@ Unicode characters.
Individual characters within a Rhai string can also be replaced just as if the string is an array of 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. In Rhai, there are also no separate concepts of `String` and `&str` as in Rust.
Immutable Strings Immutable Strings
---------------- ----------------
Rhai strings are _immutable_ and can be shared. Rhai use _immutable_ strings (type `ImmutableString`) and can be shared.
Modifying a Rhai string actually causes it first to be cloned, and then the modification made to the copy. Modifying a Rhai string actually causes it first to be cloned, and then the modification made to the copy.

View File

@ -5,6 +5,8 @@ Custom Type Getters and Setters
A custom type can also expose members by registering `get` and/or `set` functions. A custom type can also expose members by registering `get` and/or `set` functions.
Getters and setters each take a `&mut` reference to the first parameter.
```rust ```rust
#[derive(Clone)] #[derive(Clone)]
struct TestStruct { struct TestStruct {

View File

@ -7,6 +7,8 @@ A custom type 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. A custom type with an indexer function defined can use the bracket '`[]`' notation to get a property value.
Like getters and setters, indexers take a `&mut` reference to the first parameter.
Indexers are disabled when the [`no_index`] feature is used. Indexers are disabled when the [`no_index`] feature is used.
For efficiency reasons, indexers **cannot** be used to overload (i.e. override) built-in indexing operations for For efficiency reasons, indexers **cannot** be used to overload (i.e. override) built-in indexing operations for
@ -33,12 +35,10 @@ impl TestStruct {
let mut engine = Engine::new(); let mut engine = Engine::new();
engine.register_type::<TestStruct>();
engine.register_fn("new_ts", TestStruct::new);
// Shorthand: engine.register_indexer_get_set(TestStruct::get_field, TestStruct::set_field);
engine engine
.register_type::<TestStruct>()
.register_fn("new_ts", TestStruct::new)
// Shorthand: .register_indexer_get_set(TestStruct::get_field, TestStruct::set_field);
.register_indexer_get(TestStruct::get_field) .register_indexer_get(TestStruct::get_field)
.register_indexer_set(TestStruct::set_field); .register_indexer_set(TestStruct::set_field);

View File

@ -3,11 +3,19 @@
{{#include ../links.md}} {{#include ../links.md}}
Rust functions accepting parameters of `String` should use `&str` instead because it maps directly to [`ImmutableString`]
which is the type that Rhai uses to represent [strings] internally. `&str` Maps to `ImmutableString`
-------------------------------
Rust functions accepting parameters of `String` should use `&str` instead because it maps directly to
[`ImmutableString`][string] which is the type that Rhai uses to represent [strings] internally.
The parameter type `String` is discouraged because it involves converting an [`ImmutableString`] into a `String`.
Using `ImmutableString` or `&str` is much more efficient.
A common mistake made by novice Rhai users is to register functions with `String` parameters.
```rust ```rust
fn get_len1(s: String) -> i64 { s.len() as i64 } // <- Rhai will not find this function fn get_len1(s: String) -> i64 { s.len() as i64 } // <- Rhai finds this function, but very inefficient
fn get_len2(s: &str) -> i64 { s.len() as i64 } // <- Rhai finds this function fine 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 fn get_len3(s: ImmutableString) -> i64 { s.len() as i64 } // <- the above is equivalent to this
@ -20,3 +28,23 @@ let len = engine.eval::<i64>("x.len1()")?; // error: function '
let len = engine.eval::<i64>("x.len2()")?; // works fine let len = engine.eval::<i64>("x.len2()")?; // works fine
let len = engine.eval::<i64>("x.len3()")?; // works fine let len = engine.eval::<i64>("x.len3()")?; // works fine
``` ```
Avoid `&mut ImmutableString`
---------------------------
Rhai functions can take a first `&mut` parameter. Usually this is a good idea because it avoids
cloning of the argument (except for primary types where cloning is cheap), so its use is encouraged
even though there is no intention to ever mutate that argument.
`ImmutableString` is an exception to this rule. While `ImmutableString` is cheap to clone (only
incrementing a reference count), taking a mutable reference to it involves making a private clone
of the underlying string because Rhai has no way to find out whether that parameter will be mutated.
If the `ImmutableString` is not shared by any other variables, then Rhai just returns a mutable
reference to it since nobody else is watching! Otherwise a private copy is made first,
because other reference holders will not expect the `ImmutableString` to ever change
(it is supposed to be _immutable_).
Therefore, avoid using `&mut ImmutableString` as the first parameter of a function unless you really
intend to mutate that string. Use `ImmutableString` instead.

View File

@ -3,8 +3,8 @@ Maximum Size of Arrays
{{#include ../links.md}} {{#include ../links.md}}
Limiting How Large Arrays Can Grow Limit How Large Arrays Can Grow
--------------------------------- ------------------------------
Rhai by default does not limit how large an [array] can be. Rhai by default does not limit how large an [array] can be.

View File

@ -3,8 +3,8 @@ Maximum Call Stack Depth
{{#include ../links.md}} {{#include ../links.md}}
Limiting How Stack Usage by Scripts Limit How Stack Usage by Scripts
---------------------------------- -------------------------------
Rhai by default limits function calls to a maximum depth of 128 levels (16 levels in debug build). Rhai by default limits function calls to a maximum depth of 128 levels (16 levels in debug build).

View File

@ -3,8 +3,8 @@ Maximum Size of Object Maps
{{#include ../links.md}} {{#include ../links.md}}
Limiting How Large Object Maps Can Grow Limit How Large Object Maps Can Grow
-------------------------------------- -----------------------------------
Rhai by default does not limit how large (i.e. the number of properties) an [object map] can be. Rhai by default does not limit how large (i.e. the number of properties) an [object map] can be.

View File

@ -3,8 +3,8 @@ Maximum Number of Operations
{{#include ../links.md}} {{#include ../links.md}}
Limiting How Long a Script Can Run Limit How Long a Script Can Run
--------------------------------- ------------------------------
Rhai by default does not limit how much time or CPU a script consumes. Rhai by default does not limit how much time or CPU a script consumes.

View File

@ -3,8 +3,8 @@ Maximum Statement Depth
{{#include ../links.md}} {{#include ../links.md}}
Limiting How Deeply-Nested a Statement Can Be Limit How Deeply-Nested a Statement Can Be
-------------------------------------------- -----------------------------------------
Rhai by default limits statements and expressions nesting to a maximum depth of 128 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 (which should be plenty) when they are at _global_ level, but only a depth of 32

View File

@ -3,8 +3,8 @@ Maximum Length of Strings
{{#include ../links.md}} {{#include ../links.md}}
Limiting How Long Strings Can Grow Limit How Long Strings Can Grow
--------------------------------- ------------------------------
Rhai by default does not limit how long a [string] can be. Rhai by default does not limit how long a [string] can be.

View File

@ -1,5 +1,5 @@
Tracking Progress and Force-Termination Track Progress and Force-Termination
====================================== ===================================
{{#include ../links.md}} {{#include ../links.md}}

View File

@ -29,3 +29,16 @@ Turning on [`no_float`], and [`only_i32`] makes the key [`Dynamic`] data type on
while normally it can be up to 16 bytes (e.g. on x86/x64 CPU's) in order to hold an `i64` or `f64`. 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. Making [`Dynamic`] small helps performance due to better cache efficiency.
Use `ImmutableString`
--------------------
Internally, Rhai uses _immutable_ [strings] instead of the Rust `String` type. This is mainly to avoid excessive
cloning when passing function arguments.
The encapsulated immutable string type is `ImmutableString`. It is cheap to clone (just an `Rc` or `Arc` reference
count increment depending on the [`sync`] feature).
Therefore, functions taking `String` parameters should use `ImmutableString` or `&str` (which maps to `ImmutableString`)
for the best performance with Rhai.

View File

@ -10,7 +10,7 @@ A number of examples can be found in the `examples` folder:
| [`arrays_and_structs`](https://github.com/jonathandturner/rhai/tree/master/examples/arrays_and_structs.rs) | Shows how to register a custom Rust type and using [arrays] on it. | | [`arrays_and_structs`](https://github.com/jonathandturner/rhai/tree/master/examples/arrays_and_structs.rs) | Shows how to register a custom Rust type and using [arrays] on it. |
| [`custom_types_and_methods`](https://github.com/jonathandturner/rhai/tree/master/examples/custom_types_and_methods.rs) | Shows how to register a custom Rust type and methods for it. | | [`custom_types_and_methods`](https://github.com/jonathandturner/rhai/tree/master/examples/custom_types_and_methods.rs) | Shows how to register a custom Rust type and methods for it. |
| [`hello`](https://github.com/jonathandturner/rhai/tree/master/examples/hello.rs) | Simple example that evaluates an expression and prints the result. | | [`hello`](https://github.com/jonathandturner/rhai/tree/master/examples/hello.rs) | Simple example that evaluates an expression and prints the result. |
| [`no_std`](https://github.com/jonathandturner/rhai/tree/master/examples/no_std.rs) | Example to test out `no-std` builds. | | [`no_std`](https://github.com/jonathandturner/rhai/tree/master/examples/no_std.rs) | Example to test out `no-std` builds.</br>The [`no_std`] feature is required to build in `no-std`. |
| [`reuse_scope`](https://github.com/jonathandturner/rhai/tree/master/examples/reuse_scope.rs) | Evaluates two pieces of code in separate runs, but using a common [`Scope`]. | | [`reuse_scope`](https://github.com/jonathandturner/rhai/tree/master/examples/reuse_scope.rs) | Evaluates two pieces of code in separate runs, but using a common [`Scope`]. |
| [`rhai_runner`](https://github.com/jonathandturner/rhai/tree/master/examples/rhai_runner.rs) | Runs each filename passed to it as a Rhai script. | | [`rhai_runner`](https://github.com/jonathandturner/rhai/tree/master/examples/rhai_runner.rs) | Runs each filename passed to it as a Rhai script. |
| [`serde`](https://github.com/jonathandturner/rhai/tree/master/examples/serde.rs) | Example to serialize and deserialize Rust types with [`serde`](https://crates.io/crates/serde).<br/>The [`serde`] feature is required to run. | | [`serde`](https://github.com/jonathandturner/rhai/tree/master/examples/serde.rs) | Example to serialize and deserialize Rust types with [`serde`](https://crates.io/crates/serde).<br/>The [`serde`] feature is required to run. |

View File

@ -212,9 +212,11 @@ pub(crate) fn map_std_type_name(name: &str) -> &str {
"string" "string"
} else if name == type_name::<FnPtr>() { } else if name == type_name::<FnPtr>() {
"Fn" "Fn"
} else if name == type_name::<Instant>() {
"timestamp"
} else { } else {
#[cfg(not(feature = "no_std"))]
if name == type_name::<Instant>() {
return "timestamp";
}
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
if name == type_name::<Array>() { if name == type_name::<Array>() {
return "array"; return "array";

View File

@ -26,7 +26,7 @@ use crate::stdlib::{
boxed::Box, boxed::Box,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
convert::TryFrom, convert::TryFrom,
format, fmt, format,
iter::{empty, once}, iter::{empty, once},
mem, mem,
string::{String, ToString}, string::{String, ToString},
@ -96,6 +96,7 @@ pub const MARKER_BLOCK: &str = "$block$";
pub const MARKER_IDENT: &str = "$ident$"; pub const MARKER_IDENT: &str = "$ident$";
#[cfg(feature = "internals")] #[cfg(feature = "internals")]
#[derive(Debug, Clone, Hash)]
pub struct Expression<'a>(&'a Expr); pub struct Expression<'a>(&'a Expr);
#[cfg(feature = "internals")] #[cfg(feature = "internals")]
@ -346,6 +347,15 @@ pub struct Engine {
pub(crate) max_map_size: usize, pub(crate) max_map_size: usize,
} }
impl fmt::Debug for Engine {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.id.as_ref() {
Some(id) => write!(f, "Engine({})", id),
None => f.write_str("Engine"),
}
}
}
impl Default for Engine { impl Default for Engine {
fn default() -> Self { fn default() -> Self {
// Create the new scripting Engine // Create the new scripting Engine
@ -855,7 +865,7 @@ impl Engine {
fn_name, fn_name,
args.iter() args.iter()
.map(|name| if name.is::<ImmutableString>() { .map(|name| if name.is::<ImmutableString>() {
"&str | ImmutableString" "&str | ImmutableString | String"
} else { } else {
self.map_type_name((*name).type_name()) self.map_type_name((*name).type_name())
}) })
@ -934,7 +944,7 @@ impl Engine {
} }
// Has a system function an override? // Has a system function an override?
fn has_override(&self, lib: &Module, (hash_fn, hash_script): (u64, u64)) -> bool { fn has_override(&self, lib: &Module, hash_fn: u64, hash_script: u64) -> bool {
// NOTE: We skip script functions for global_module and packages, and native functions for lib // NOTE: We skip script functions for global_module and packages, and native functions for lib
// First check script-defined functions // First check script-defined functions
@ -976,13 +986,15 @@ impl Engine {
match fn_name { match fn_name {
// type_of // type_of
KEYWORD_TYPE_OF if args.len() == 1 && !self.has_override(lib, hashes) => Ok(( KEYWORD_TYPE_OF if args.len() == 1 && !self.has_override(lib, hashes.0, hashes.1) => {
self.map_type_name(args[0].type_name()).to_string().into(), Ok((
false, self.map_type_name(args[0].type_name()).to_string().into(),
)), false,
))
}
// Fn // Fn
KEYWORD_FN_PTR if args.len() == 1 && !self.has_override(lib, hashes) => { KEYWORD_FN_PTR if args.len() == 1 && !self.has_override(lib, hashes.0, hashes.1) => {
Err(Box::new(EvalAltResult::ErrorRuntime( Err(Box::new(EvalAltResult::ErrorRuntime(
"'Fn' should not be called in method style. Try Fn(...);".into(), "'Fn' should not be called in method style. Try Fn(...);".into(),
Position::none(), Position::none(),
@ -990,7 +1002,7 @@ impl Engine {
} }
// eval - reaching this point it must be a method-style call // eval - reaching this point it must be a method-style call
KEYWORD_EVAL if args.len() == 1 && !self.has_override(lib, hashes) => { KEYWORD_EVAL if args.len() == 1 && !self.has_override(lib, hashes.0, hashes.1) => {
Err(Box::new(EvalAltResult::ErrorRuntime( Err(Box::new(EvalAltResult::ErrorRuntime(
"'eval' should not be called in method style. Try eval(...);".into(), "'eval' should not be called in method style. Try eval(...);".into(),
Position::none(), Position::none(),
@ -1076,10 +1088,10 @@ impl Engine {
let idx = idx_val.downcast_mut::<StaticVec<Dynamic>>().unwrap(); let idx = idx_val.downcast_mut::<StaticVec<Dynamic>>().unwrap();
let mut fn_name = name.as_ref(); let mut fn_name = name.as_ref();
// Check if it is a FnPtr call
let (result, updated) = if fn_name == KEYWORD_FN_PTR_CALL && obj.is::<FnPtr>() { let (result, updated) = if fn_name == KEYWORD_FN_PTR_CALL && obj.is::<FnPtr>() {
// FnPtr call
// Redirect function name // Redirect function name
fn_name = obj.as_str().unwrap(); let fn_name = obj.as_str().unwrap();
// Recalculate hash // Recalculate hash
let hash = calc_fn_hash(empty(), fn_name, idx.len(), empty()); let hash = calc_fn_hash(empty(), fn_name, idx.len(), empty());
// Arguments are passed as-is // Arguments are passed as-is
@ -1090,11 +1102,32 @@ impl Engine {
self.exec_fn_call( self.exec_fn_call(
state, lib, fn_name, *native, hash, args, false, false, def_val, level, state, lib, fn_name, *native, hash, args, false, false, def_val, level,
) )
} else if fn_name == KEYWORD_FN_PTR_CALL && idx.len() > 0 && idx[0].is::<FnPtr>() {
// FnPtr call on object
// Redirect function name
let fn_name = idx[0]
.downcast_ref::<FnPtr>()
.unwrap()
.get_fn_name()
.clone();
// Recalculate hash
let hash = calc_fn_hash(empty(), &fn_name, idx.len() - 1, empty());
// Replace the first argument with the object pointer
let mut arg_values = once(obj)
.chain(idx.iter_mut().skip(1))
.collect::<StaticVec<_>>();
let args = arg_values.as_mut();
// Map it to name(args) in function-call style
self.exec_fn_call(
state, lib, &fn_name, *native, hash, args, is_ref, true, def_val, level,
)
} else { } else {
let redirected: Option<ImmutableString>; let redirected: Option<ImmutableString>;
let mut hash = *hash; let mut hash = *hash;
// Check if it is a map method call in OOP style // Check if it is a map method call in OOP style
#[cfg(not(feature = "no_object"))]
if let Some(map) = obj.downcast_ref::<Map>() { if let Some(map) = obj.downcast_ref::<Map>() {
if let Some(val) = map.get(fn_name) { if let Some(val) = map.get(fn_name) {
if let Some(f) = val.downcast_ref::<FnPtr>() { if let Some(f) = val.downcast_ref::<FnPtr>() {
@ -1915,7 +1948,7 @@ impl Engine {
let hash_fn = let hash_fn =
calc_fn_hash(empty(), name, 1, once(TypeId::of::<ImmutableString>())); calc_fn_hash(empty(), name, 1, once(TypeId::of::<ImmutableString>()));
if !self.has_override(lib, (hash_fn, *hash)) { if !self.has_override(lib, hash_fn, *hash) {
// Fn - only in function call style // Fn - only in function call style
let expr = args_expr.get(0); let expr = args_expr.get(0);
let arg_value = let arg_value =
@ -1941,7 +1974,7 @@ impl Engine {
let hash_fn = let hash_fn =
calc_fn_hash(empty(), name, 1, once(TypeId::of::<ImmutableString>())); calc_fn_hash(empty(), name, 1, once(TypeId::of::<ImmutableString>()));
if !self.has_override(lib, (hash_fn, *hash)) { if !self.has_override(lib, hash_fn, *hash) {
// eval - only in function call style // eval - only in function call style
let prev_len = scope.len(); let prev_len = scope.len();
let expr = args_expr.get(0); let expr = args_expr.get(0);
@ -1961,6 +1994,36 @@ impl Engine {
} }
} }
// Handle call() - Redirect function call
let redirected;
let mut name = name.as_ref();
let mut args_expr = args_expr.as_ref();
let mut hash = *hash;
if name == KEYWORD_FN_PTR_CALL
&& args_expr.len() >= 1
&& !self.has_override(lib, 0, hash)
{
let expr = args_expr.get(0).unwrap();
let fn_ptr = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?;
if fn_ptr.is::<FnPtr>() {
// Redirect function name
redirected = Some(fn_ptr.cast::<FnPtr>().take_fn_name());
name = redirected.as_ref().unwrap();
// Skip the first argument
args_expr = &args_expr.as_ref()[1..];
// Recalculate hash
hash = calc_fn_hash(empty(), name, args_expr.len(), empty());
} else {
return Err(Box::new(EvalAltResult::ErrorMismatchOutputType(
self.map_type_name(type_name::<FnPtr>()).into(),
fn_ptr.type_name().into(),
expr.position(),
)));
}
}
// Normal function call - except for Fn and eval (handled above) // Normal function call - except for Fn and eval (handled above)
let mut arg_values: StaticVec<Dynamic>; let mut arg_values: StaticVec<Dynamic>;
let mut args: StaticVec<_>; let mut args: StaticVec<_>;
@ -1972,7 +2035,7 @@ impl Engine {
} else { } else {
// See if the first argument is a variable, if so, convert to method-call style // See if the first argument is a variable, if so, convert to method-call style
// in order to leverage potential &mut first argument and avoid cloning the value // in order to leverage potential &mut first argument and avoid cloning the value
match args_expr.get(0) { match args_expr.get(0).unwrap() {
// func(x, ...) -> x.func(...) // func(x, ...) -> x.func(...)
lhs @ Expr::Variable(_) => { lhs @ Expr::Variable(_) => {
arg_values = args_expr arg_values = args_expr
@ -2009,7 +2072,7 @@ impl Engine {
let args = args.as_mut(); let args = args.as_mut();
self.exec_fn_call( self.exec_fn_call(
state, lib, name, *native, *hash, args, is_ref, false, def_val, level, state, lib, name, *native, hash, args, is_ref, false, def_val, level,
) )
.map(|(v, _)| v) .map(|(v, _)| v)
.map_err(|err| err.new_position(*pos)) .map_err(|err| err.new_position(*pos))

View File

@ -98,6 +98,8 @@ pub enum ParseErrorType {
PropertyExpected, PropertyExpected,
/// Missing a variable name after the `let`, `const` or `for` keywords. /// Missing a variable name after the `let`, `const` or `for` keywords.
VariableExpected, VariableExpected,
/// An identifier is a reserved keyword.
Reserved(String),
/// Missing an expression. Wrapped value is the expression type. /// Missing an expression. Wrapped value is the expression type.
ExprExpected(String), ExprExpected(String),
/// Defining a function `fn` in an appropriate place (e.g. inside another function). /// Defining a function `fn` in an appropriate place (e.g. inside another function).
@ -163,6 +165,7 @@ impl ParseErrorType {
Self::ForbiddenConstantExpr(_) => "Expecting a constant", Self::ForbiddenConstantExpr(_) => "Expecting a constant",
Self::PropertyExpected => "Expecting name of a property", Self::PropertyExpected => "Expecting name of a property",
Self::VariableExpected => "Expecting name of a variable", Self::VariableExpected => "Expecting name of a variable",
Self::Reserved(_) => "Invalid use of reserved keyword",
Self::ExprExpected(_) => "Expecting an expression", Self::ExprExpected(_) => "Expecting an expression",
Self::FnMissingName => "Expecting name in function declaration", Self::FnMissingName => "Expecting name in function declaration",
Self::FnMissingParams(_) => "Expecting parameters in function declaration", Self::FnMissingParams(_) => "Expecting parameters in function declaration",
@ -224,6 +227,7 @@ impl fmt::Display for ParseErrorType {
Self::LiteralTooLarge(typ, max) => { Self::LiteralTooLarge(typ, max) => {
write!(f, "{} exceeds the maximum limit ({})", typ, max) write!(f, "{} exceeds the maximum limit ({})", typ, max)
} }
Self::Reserved(s) => write!(f, "'{}' is a reserved keyword", s),
_ => f.write_str(self.desc()), _ => f.write_str(self.desc()),
} }
} }

View File

@ -8,7 +8,7 @@ use crate::result::EvalAltResult;
use crate::token::{is_valid_identifier, Position}; use crate::token::{is_valid_identifier, Position};
use crate::utils::ImmutableString; use crate::utils::ImmutableString;
use crate::stdlib::{boxed::Box, convert::TryFrom, fmt, rc::Rc, sync::Arc}; use crate::stdlib::{boxed::Box, convert::TryFrom, fmt, rc::Rc, string::String, sync::Arc};
/// Trait that maps to `Send + Sync` only under the `sync` feature. /// Trait that maps to `Send + Sync` only under the `sync` feature.
#[cfg(feature = "sync")] #[cfg(feature = "sync")]
@ -87,7 +87,7 @@ impl TryFrom<ImmutableString> for FnPtr {
Ok(Self(value)) Ok(Self(value))
} else { } else {
Err(Box::new(EvalAltResult::ErrorFunctionNotFound( Err(Box::new(EvalAltResult::ErrorFunctionNotFound(
value.to_string(), value.into(),
Position::none(), Position::none(),
))) )))
} }

View File

@ -7,10 +7,16 @@ use crate::fn_native::{CallableFunction, FnAny, FnCallArgs, SendSync};
use crate::module::Module; use crate::module::Module;
use crate::parser::FnAccess; use crate::parser::FnAccess;
use crate::plugin::Plugin; use crate::plugin::Plugin;
use crate::r#unsafe::unsafe_cast_box;
use crate::result::EvalAltResult; use crate::result::EvalAltResult;
use crate::utils::ImmutableString; use crate::utils::ImmutableString;
use crate::stdlib::{any::TypeId, boxed::Box, mem}; use crate::stdlib::{
any::TypeId,
boxed::Box,
mem,
string::{String, ToString},
};
/// A trait to register custom plugins with the `Engine`. /// A trait to register custom plugins with the `Engine`.
/// ///
@ -185,6 +191,9 @@ pub fn by_value<T: Variant + Clone>(data: &mut Dynamic) -> T {
let ref_str = data.as_str().unwrap(); let ref_str = data.as_str().unwrap();
let ref_T = unsafe { mem::transmute::<_, &T>(&ref_str) }; let ref_T = unsafe { mem::transmute::<_, &T>(&ref_str) };
ref_T.clone() ref_T.clone()
} else if TypeId::of::<T>() == TypeId::of::<String>() {
// If T is String, data must be ImmutableString, so map directly to it
*unsafe_cast_box(Box::new(data.as_str().unwrap().to_string())).unwrap()
} else { } else {
// We consume the argument and then replace it with () - the argument is not supposed to be used again. // We consume the argument and then replace it with () - the argument is not supposed to be used again.
// This way, we avoid having to clone the argument again, because it is already a clone when passed here. // This way, we avoid having to clone the argument again, because it is already a clone when passed here.
@ -240,13 +249,15 @@ pub fn map_result(
data data
} }
/// Remap `&str` to `ImmutableString`. /// Remap `&str` | `String` to `ImmutableString`.
#[inline(always)] #[inline(always)]
fn map_type_id<T: 'static>() -> TypeId { fn map_type_id<T: 'static>() -> TypeId {
let id = TypeId::of::<T>(); let id = TypeId::of::<T>();
if id == TypeId::of::<&str>() { if id == TypeId::of::<&str>() {
TypeId::of::<ImmutableString>() TypeId::of::<ImmutableString>()
} else if id == TypeId::of::<String>() {
TypeId::of::<ImmutableString>()
} else { } else {
id id
} }

View File

@ -25,11 +25,13 @@ use crate::stdlib::{
num::NonZeroUsize, num::NonZeroUsize,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
string::{String, ToString}, string::{String, ToString},
sync::RwLock,
vec, vec,
vec::Vec, vec::Vec,
}; };
#[cfg(not(feature = "no_std"))]
use crate::stdlib::sync::RwLock;
/// Return type of module-level Rust function. /// Return type of module-level Rust function.
pub type FuncReturn<T> = Result<T, Box<EvalAltResult>>; pub type FuncReturn<T> = Result<T, Box<EvalAltResult>>;

View File

@ -52,7 +52,7 @@ pub type PackageLibrary = Shared<Module>;
/// Type containing a collection of `PackageLibrary` instances. /// Type containing a collection of `PackageLibrary` instances.
/// All function and type iterator keys in the loaded packages are indexed for fast access. /// All function and type iterator keys in the loaded packages are indexed for fast access.
#[derive(Clone, Default)] #[derive(Debug, Clone, Default)]
pub(crate) struct PackagesCollection(StaticVec<PackageLibrary>); pub(crate) struct PackagesCollection(StaticVec<PackageLibrary>);
impl PackagesCollection { impl PackagesCollection {

View File

@ -7,7 +7,7 @@ use crate::error::{LexError, ParseError, ParseErrorType};
use crate::module::{Module, ModuleRef}; use crate::module::{Module, ModuleRef};
use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::optimize::{optimize_into_ast, OptimizationLevel};
use crate::scope::{EntryType as ScopeEntryType, Scope}; use crate::scope::{EntryType as ScopeEntryType, Scope};
use crate::token::{Position, Token, TokenStream}; use crate::token::{is_valid_identifier, Position, Token, TokenStream};
use crate::utils::{StaticVec, StraightHasherBuilder}; use crate::utils::{StaticVec, StraightHasherBuilder};
#[cfg(feature = "internals")] #[cfg(feature = "internals")]
@ -25,6 +25,7 @@ use crate::stdlib::{
char, char,
collections::HashMap, collections::HashMap,
fmt, format, fmt, format,
hash::Hash,
iter::empty, iter::empty,
mem, mem,
num::NonZeroUsize, num::NonZeroUsize,
@ -340,7 +341,7 @@ impl fmt::Display for FnAccess {
} }
/// A scripted function definition. /// A scripted function definition.
#[derive(Debug, Clone)] #[derive(Debug, Clone, Hash)]
pub struct ScriptFnDef { pub struct ScriptFnDef {
/// Function name. /// Function name.
pub name: String, pub name: String,
@ -374,7 +375,7 @@ impl fmt::Display for ScriptFnDef {
} }
/// `return`/`throw` statement. /// `return`/`throw` statement.
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] #[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
pub enum ReturnType { pub enum ReturnType {
/// `return` statement. /// `return` statement.
Return, Return,
@ -440,6 +441,8 @@ struct ParseSettings {
pos: Position, pos: Position,
/// Is the construct being parsed located at global level? /// Is the construct being parsed located at global level?
is_global: bool, is_global: bool,
/// Is the construct being parsed located at function definition level?
is_function_scope: bool,
/// Is the current position inside a loop? /// Is the current position inside a loop?
is_breakable: bool, is_breakable: bool,
/// Is anonymous function allowed? /// Is anonymous function allowed?
@ -476,7 +479,7 @@ impl ParseSettings {
/// ///
/// Each variant is at most one pointer in size (for speed), /// Each variant is at most one pointer in size (for speed),
/// with everything being allocated together in one single tuple. /// with everything being allocated together in one single tuple.
#[derive(Debug, Clone)] #[derive(Debug, Clone, Hash)]
pub enum Stmt { pub enum Stmt {
/// No-op. /// No-op.
Noop(Position), Noop(Position),
@ -588,6 +591,13 @@ impl fmt::Debug for CustomExpr {
} }
} }
#[cfg(feature = "internals")]
impl Hash for CustomExpr {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
/// An expression. /// An expression.
/// ///
/// Each variant is at most one pointer in size (for speed), /// Each variant is at most one pointer in size (for speed),
@ -663,6 +673,18 @@ impl Default for Expr {
} }
} }
impl Hash for Expr {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
Self::FloatConstant(x) => {
state.write(&x.0.to_le_bytes());
x.1.hash(state);
}
_ => self.hash(state),
}
}
}
impl Expr { impl Expr {
/// Get the `Dynamic` value of a constant expression. /// Get the `Dynamic` value of a constant expression.
/// ///
@ -1343,56 +1365,57 @@ fn parse_map_literal(
eat_token(input, Token::RightBrace); eat_token(input, Token::RightBrace);
break; break;
} }
_ => { _ => (),
let (name, pos) = match input.next().unwrap() {
(Token::Identifier(s), pos) => (s, pos),
(Token::StringConstant(s), pos) => (s, pos),
(Token::LexError(err), pos) => return Err(err.into_err(pos)),
(_, pos) if map.is_empty() => {
return Err(PERR::MissingToken(
Token::RightBrace.into(),
MISSING_RBRACE.into(),
)
.into_err(pos))
}
(Token::EOF, pos) => {
return Err(PERR::MissingToken(
Token::RightBrace.into(),
MISSING_RBRACE.into(),
)
.into_err(pos))
}
(_, pos) => return Err(PERR::PropertyExpected.into_err(pos)),
};
match input.next().unwrap() {
(Token::Colon, _) => (),
(Token::LexError(err), pos) => return Err(err.into_err(pos)),
(_, pos) => {
return Err(PERR::MissingToken(
Token::Colon.into(),
format!(
"to follow the property '{}' in this object map literal",
name
),
)
.into_err(pos))
}
};
if state.engine.max_map_size > 0 && map.len() >= state.engine.max_map_size {
return Err(PERR::LiteralTooLarge(
"Number of properties in object map literal".to_string(),
state.engine.max_map_size,
)
.into_err(input.peek().unwrap().1));
}
let expr = parse_expr(input, state, lib, settings.level_up())?;
map.push(((Into::<ImmutableString>::into(name), pos), expr));
}
} }
let (name, pos) = match input.next().unwrap() {
(Token::Identifier(s), pos) => (s, pos),
(Token::StringConstant(s), pos) => (s, pos),
(Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => {
return Err(PERR::Reserved(s).into_err(pos));
}
(Token::LexError(err), pos) => return Err(err.into_err(pos)),
(_, pos) if map.is_empty() => {
return Err(
PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into())
.into_err(pos),
);
}
(Token::EOF, pos) => {
return Err(
PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into())
.into_err(pos),
);
}
(_, pos) => return Err(PERR::PropertyExpected.into_err(pos)),
};
match input.next().unwrap() {
(Token::Colon, _) => (),
(Token::LexError(err), pos) => return Err(err.into_err(pos)),
(_, pos) => {
return Err(PERR::MissingToken(
Token::Colon.into(),
format!(
"to follow the property '{}' in this object map literal",
name
),
)
.into_err(pos))
}
};
if state.engine.max_map_size > 0 && map.len() >= state.engine.max_map_size {
return Err(PERR::LiteralTooLarge(
"Number of properties in object map literal".to_string(),
state.engine.max_map_size,
)
.into_err(input.peek().unwrap().1));
}
let expr = parse_expr(input, state, lib, settings.level_up())?;
map.push(((Into::<ImmutableString>::into(name), pos), expr));
match input.peek().unwrap() { match input.peek().unwrap() {
(Token::Comma, _) => { (Token::Comma, _) => {
eat_token(input, Token::Comma); eat_token(input, Token::Comma);
@ -1460,6 +1483,24 @@ fn parse_primary(
let index = state.find_var(&s); let index = state.find_var(&s);
Expr::Variable(Box::new(((s, settings.pos), None, 0, index))) Expr::Variable(Box::new(((s, settings.pos), None, 0, index)))
} }
// Function call is allowed to have reserved keyword
Token::Reserved(s) if s != KEYWORD_THIS && input.peek().unwrap().0 == Token::LeftParen => {
Expr::Variable(Box::new(((s, settings.pos), None, 0, None)))
}
// Access to `this` as a variable is OK
Token::Reserved(s) if s == KEYWORD_THIS && input.peek().unwrap().0 != Token::LeftParen => {
if !settings.is_function_scope {
return Err(
PERR::BadInput(format!("'{}' can only be used in functions", s))
.into_err(settings.pos),
);
} else {
Expr::Variable(Box::new(((s, settings.pos), None, 0, None)))
}
}
Token::Reserved(s) if is_valid_identifier(s.chars()) => {
return Err(PERR::Reserved(s).into_err(settings.pos));
}
Token::LeftParen => parse_paren_expr(input, state, lib, settings.level_up())?, Token::LeftParen => parse_paren_expr(input, state, lib, settings.level_up())?,
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Token::LeftBracket => parse_array_literal(input, state, lib, settings.level_up())?, Token::LeftBracket => parse_array_literal(input, state, lib, settings.level_up())?,
@ -1471,7 +1512,7 @@ fn parse_primary(
_ => { _ => {
return Err( return Err(
PERR::BadInput(format!("Unexpected '{}'", token.syntax())).into_err(settings.pos) PERR::BadInput(format!("Unexpected '{}'", token.syntax())).into_err(settings.pos)
) );
} }
}; };
@ -1509,6 +1550,9 @@ fn parse_primary(
Expr::Variable(Box::new(((id2, pos2), modules, 0, index))) Expr::Variable(Box::new(((id2, pos2), modules, 0, index)))
} }
(Token::Reserved(id2), pos2) if is_valid_identifier(id2.chars()) => {
return Err(PERR::Reserved(id2).into_err(pos2));
}
(_, pos2) => return Err(PERR::VariableExpected.into_err(pos2)), (_, pos2) => return Err(PERR::VariableExpected.into_err(pos2)),
}, },
// Indexing // Indexing
@ -1538,6 +1582,7 @@ fn parse_primary(
_ => (), _ => (),
} }
// Make sure identifiers are valid
Ok(root_expr) Ok(root_expr)
} }
@ -2094,6 +2139,9 @@ fn parse_expr(
(Token::Identifier(s), pos) => { (Token::Identifier(s), pos) => {
exprs.push(Expr::Variable(Box::new(((s, pos), None, 0, None)))); exprs.push(Expr::Variable(Box::new(((s, pos), None, 0, None))));
} }
(Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => {
return Err(PERR::Reserved(s).into_err(pos));
}
(_, pos) => return Err(PERR::VariableExpected.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
}, },
MARKER_EXPR => exprs.push(parse_expr(input, state, lib, settings)?), MARKER_EXPR => exprs.push(parse_expr(input, state, lib, settings)?),
@ -2261,10 +2309,12 @@ fn parse_for(
let name = match input.next().unwrap() { let name = match input.next().unwrap() {
// Variable name // Variable name
(Token::Identifier(s), _) => s, (Token::Identifier(s), _) => s,
// Reserved keyword
(Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => {
return Err(PERR::Reserved(s).into_err(pos));
}
// Bad identifier // Bad identifier
(Token::LexError(err), pos) => return Err(err.into_err(pos)), (Token::LexError(err), pos) => return Err(err.into_err(pos)),
// EOF
(Token::EOF, pos) => return Err(PERR::VariableExpected.into_err(pos)),
// Not a variable name // Not a variable name
(_, pos) => return Err(PERR::VariableExpected.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
}; };
@ -2311,20 +2361,13 @@ fn parse_let(
// let name ... // let name ...
let (name, pos) = match input.next().unwrap() { let (name, pos) = match input.next().unwrap() {
(Token::Identifier(s), pos) => (s, pos), (Token::Identifier(s), pos) => (s, pos),
(Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => {
return Err(PERR::Reserved(s).into_err(pos));
}
(Token::LexError(err), pos) => return Err(err.into_err(pos)), (Token::LexError(err), pos) => return Err(err.into_err(pos)),
(_, pos) => return Err(PERR::VariableExpected.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
}; };
// Check if the name is allowed
match name.as_str() {
KEYWORD_THIS => {
return Err(
PERR::BadInput(LexError::MalformedIdentifier(name).to_string()).into_err(pos),
)
}
_ => (),
}
// let name = ... // let name = ...
if match_token(input, Token::Equals)? { if match_token(input, Token::Equals)? {
// let name = expr // let name = expr
@ -2390,6 +2433,9 @@ fn parse_import(
// import expr as name ... // import expr as name ...
let (name, _) = match input.next().unwrap() { let (name, _) = match input.next().unwrap() {
(Token::Identifier(s), pos) => (s, pos), (Token::Identifier(s), pos) => (s, pos),
(Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => {
return Err(PERR::Reserved(s).into_err(pos));
}
(Token::LexError(err), pos) => return Err(err.into_err(pos)), (Token::LexError(err), pos) => return Err(err.into_err(pos)),
(_, pos) => return Err(PERR::VariableExpected.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
}; };
@ -2414,6 +2460,9 @@ fn parse_export(
loop { loop {
let (id, id_pos) = match input.next().unwrap() { let (id, id_pos) = match input.next().unwrap() {
(Token::Identifier(s), pos) => (s.clone(), pos), (Token::Identifier(s), pos) => (s.clone(), pos),
(Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => {
return Err(PERR::Reserved(s).into_err(pos));
}
(Token::LexError(err), pos) => return Err(err.into_err(pos)), (Token::LexError(err), pos) => return Err(err.into_err(pos)),
(_, pos) => return Err(PERR::VariableExpected.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
}; };
@ -2421,6 +2470,10 @@ fn parse_export(
let rename = if match_token(input, Token::As)? { let rename = if match_token(input, Token::As)? {
match input.next().unwrap() { match input.next().unwrap() {
(Token::Identifier(s), pos) => Some((s.clone(), pos)), (Token::Identifier(s), pos) => Some((s.clone(), pos)),
(Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => {
return Err(PERR::Reserved(s).into_err(pos));
}
(Token::LexError(err), pos) => return Err(err.into_err(pos)),
(_, pos) => return Err(PERR::VariableExpected.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
} }
} else { } else {
@ -2598,6 +2651,7 @@ fn parse_stmt(
allow_stmt_expr: true, allow_stmt_expr: true,
allow_anonymous_fn: true, allow_anonymous_fn: true,
is_global: false, is_global: false,
is_function_scope: true,
is_breakable: false, is_breakable: false,
level: 0, level: 0,
pos: pos, pos: pos,
@ -2695,7 +2749,11 @@ fn parse_fn(
settings.ensure_level_within_max_limit(state.max_expr_depth)?; settings.ensure_level_within_max_limit(state.max_expr_depth)?;
let name = match input.next().unwrap() { let name = match input.next().unwrap() {
(Token::Identifier(s), _) | (Token::Custom(s), _) => s, (Token::Identifier(s), _) | (Token::Custom(s), _) | (Token::Reserved(s), _)
if s != KEYWORD_THIS && is_valid_identifier(s.chars()) =>
{
s
}
(_, pos) => return Err(PERR::FnMissingName.into_err(pos)), (_, pos) => return Err(PERR::FnMissingName.into_err(pos)),
}; };
@ -2790,6 +2848,7 @@ impl Engine {
allow_stmt_expr: false, allow_stmt_expr: false,
allow_anonymous_fn: false, allow_anonymous_fn: false,
is_global: true, is_global: true,
is_function_scope: false,
is_breakable: false, is_breakable: false,
level: 0, level: 0,
pos: Position::none(), pos: Position::none(),
@ -2829,6 +2888,7 @@ impl Engine {
allow_stmt_expr: true, allow_stmt_expr: true,
allow_anonymous_fn: true, allow_anonymous_fn: true,
is_global: true, is_global: true,
is_function_scope: false,
is_breakable: false, is_breakable: false,
level: 0, level: 0,
pos: Position::none(), pos: Position::none(),

View File

@ -4,6 +4,8 @@ use crate::optimize::OptimizationLevel;
use crate::packages::PackageLibrary; use crate::packages::PackageLibrary;
use crate::token::is_valid_identifier; use crate::token::is_valid_identifier;
use crate::stdlib::{boxed::Box, format, string::String};
impl Engine { impl Engine {
/// Load a new package into the `Engine`. /// Load a new package into the `Engine`.
/// ///

View File

@ -16,7 +16,7 @@ mod inner {
pub use core_error as error; pub use core_error as error;
pub mod collections { pub mod collections {
pub use hashbrown::HashMap; pub use hashbrown::{HashMap, HashSet};
} }
} }

View File

@ -75,7 +75,7 @@ impl Engine {
) -> Result<Dynamic, Box<EvalAltResult>> ) -> Result<Dynamic, Box<EvalAltResult>>
+ SendSync + SendSync
+ 'static, + 'static,
) -> Result<self, Box<LexError>> { ) -> Result<&mut Self, Box<LexError>> {
if value.is_empty() { if value.is_empty() {
return Err(Box::new(LexError::ImproperSymbol("".to_string()))); return Err(Box::new(LexError::ImproperSymbol("".to_string())));
} }

View File

@ -1,6 +1,10 @@
//! Main module defining the lexer and parser. //! Main module defining the lexer and parser.
use crate::engine::Engine; use crate::engine::{
Engine, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CALL, KEYWORD_PRINT,
KEYWORD_THIS, KEYWORD_TYPE_OF,
};
use crate::error::LexError; use crate::error::LexError;
use crate::parser::INT; use crate::parser::INT;
use crate::utils::StaticVec; use crate::utils::StaticVec;
@ -13,7 +17,7 @@ use crate::stdlib::{
boxed::Box, boxed::Box,
char, char,
collections::HashMap, collections::HashMap,
fmt, fmt, format,
iter::Peekable, iter::Peekable,
str::{Chars, FromStr}, str::{Chars, FromStr},
string::{String, ToString}, string::{String, ToString},
@ -275,8 +279,6 @@ impl Token {
Or => "||", Or => "||",
Ampersand => "&", Ampersand => "&",
And => "&&", And => "&&",
#[cfg(not(feature = "no_function"))]
Fn => "fn",
Continue => "continue", Continue => "continue",
Break => "break", Break => "break",
Return => "return", Return => "return",
@ -297,8 +299,12 @@ impl Token {
ModuloAssign => "%=", ModuloAssign => "%=",
PowerOf => "~", PowerOf => "~",
PowerOfAssign => "~=", PowerOfAssign => "~=",
#[cfg(not(feature = "no_function"))]
Fn => "fn",
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
Private => "private", Private => "private",
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
Import => "import", Import => "import",
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
@ -355,8 +361,6 @@ impl Token {
"||" => Or, "||" => Or,
"&" => Ampersand, "&" => Ampersand,
"&&" => And, "&&" => And,
#[cfg(not(feature = "no_function"))]
"fn" => Fn,
"continue" => Continue, "continue" => Continue,
"break" => Break, "break" => Break,
"return" => Return, "return" => Return,
@ -377,17 +381,30 @@ impl Token {
"%=" => ModuloAssign, "%=" => ModuloAssign,
"~" => PowerOf, "~" => PowerOf,
"~=" => PowerOfAssign, "~=" => PowerOfAssign,
#[cfg(not(feature = "no_function"))]
"fn" => Fn,
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
"private" => Private, "private" => Private,
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
"import" => Import, "import" => Import,
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
"export" => Export, "export" => Export,
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
"as" => As, "as" => As,
#[cfg(feature = "no_function")]
"fn" | "private" => Reserved(syntax.into()),
#[cfg(feature = "no_module")]
"import" | "export" | "as" => Reserved(syntax.into()),
"===" | "!==" | "->" | "<-" | "=>" | ":=" | "::<" | "(*" | "*)" | "#" => { "===" | "!==" | "->" | "<-" | "=>" | ":=" | "::<" | "(*" | "*)" | "#" => {
Reserved(syntax.into()) Reserved(syntax.into())
} }
KEYWORD_PRINT | KEYWORD_DEBUG | KEYWORD_TYPE_OF | KEYWORD_EVAL | KEYWORD_FN_PTR
| KEYWORD_FN_PTR_CALL | KEYWORD_THIS => Reserved(syntax.into()),
_ => return None, _ => return None,
}) })
@ -1353,9 +1370,10 @@ impl<'a> Iterator for TokenIterator<'a, '_> {
"'#' is not a valid symbol. Should it be '#{'?" "'#' is not a valid symbol. Should it be '#{'?"
.to_string(), .to_string(),
))), ))),
token => Token::LexError(Box::new(LERR::ImproperSymbol( token if !is_valid_identifier(token.chars()) => Token::LexError(Box::new(LERR::ImproperSymbol(
format!("'{}' is not a valid symbol.", token) format!("'{}' is a reserved symbol.", token)
))), ))),
_ => Token::Reserved(s)
}, pos)), }, pos)),
(r @ Some(_), None, None) => r, (r @ Some(_), None, None) => r,
(Some((Token::Identifier(s), pos)), _, Some(custom)) if custom.contains_key(&s) => { (Some((Token::Identifier(s), pos)), _, Some(custom)) if custom.contains_key(&s) => {

View File

@ -156,6 +156,12 @@ impl<T> Drop for StaticVec<T> {
} }
} }
impl<T: Hash> Hash for StaticVec<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.iter().for_each(|x| x.hash(state));
}
}
impl<T> Default for StaticVec<T> { impl<T> Default for StaticVec<T> {
fn default() -> Self { fn default() -> Self {
Self { Self {

View File

@ -115,7 +115,8 @@ fn test_anonymous_fn() -> Result<(), Box<EvalAltResult>> {
} }
#[test] #[test]
fn test_fn_ptr() -> Result<(), Box<EvalAltResult>> { #[cfg(not(feature = "no_object"))]
fn test_fn_ptr_raw() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new(); let mut engine = Engine::new();
engine.register_raw_fn( engine.register_raw_fn(

View File

@ -8,7 +8,7 @@ fn test_decrement() -> Result<(), Box<EvalAltResult>> {
assert!(matches!( assert!(matches!(
*engine.eval::<String>(r#"let s = "test"; s -= "ing"; s"#).expect_err("expects error"), *engine.eval::<String>(r#"let s = "test"; s -= "ing"; s"#).expect_err("expects error"),
EvalAltResult::ErrorFunctionNotFound(err, _) if err == "- (&str | ImmutableString, &str | ImmutableString)" EvalAltResult::ErrorFunctionNotFound(err, _) if err == "- (&str | ImmutableString | String, &str | ImmutableString | String)"
)); ));
Ok(()) Ok(())

80
tests/fn_ptr.rs Normal file
View File

@ -0,0 +1,80 @@
use rhai::{Engine, EvalAltResult, RegisterFn, INT};
#[test]
fn test_fn_ptr() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
engine.register_fn("bar", |x: &mut INT, y: INT| *x += y);
#[cfg(not(feature = "no_object"))]
assert_eq!(
engine.eval::<INT>(
r#"
let f = Fn("bar");
let x = 40;
f.call(x, 2);
x
"#
)?,
40
);
#[cfg(not(feature = "no_object"))]
assert_eq!(
engine.eval::<INT>(
r#"
let f = Fn("bar");
let x = 40;
x.call(f, 2);
x
"#
)?,
42
);
assert_eq!(
engine.eval::<INT>(
r#"
let f = Fn("bar");
let x = 40;
call(f, x, 2);
x
"#
)?,
42
);
#[cfg(not(feature = "no_function"))]
#[cfg(not(feature = "no_object"))]
assert_eq!(
engine.eval::<INT>(
r#"
fn foo(x) { this += x; }
let f = Fn("foo");
let x = 40;
x.call(f, 2);
x
"#
)?,
42
);
#[cfg(not(feature = "no_function"))]
assert!(matches!(
*engine
.eval::<INT>(
r#"
fn foo(x) { this += x; }
let f = Fn("foo");
call(f, 2);
x
"#
)
.expect_err("should error"),
EvalAltResult::ErrorInFunctionCall(fn_name, err, _) if fn_name == "foo" && matches!(*err, EvalAltResult::ErrorUnboundedThis(_))
));
Ok(())
}

View File

@ -52,12 +52,24 @@ fn test_functions() -> Result<(), Box<EvalAltResult>> {
} }
#[test] #[test]
#[cfg(not(feature = "no_object"))]
fn test_function_pointers() -> Result<(), Box<EvalAltResult>> { fn test_function_pointers() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new(); let engine = Engine::new();
assert_eq!(engine.eval::<String>(r#"type_of(Fn("abc"))"#)?, "Fn"); assert_eq!(engine.eval::<String>(r#"type_of(Fn("abc"))"#)?, "Fn");
assert_eq!(
engine.eval::<INT>(
r#"
fn foo(x) { 40 + x }
let f = Fn("foo");
call(f, 2)
"#
)?,
42
);
#[cfg(not(feature = "no_object"))]
assert_eq!( assert_eq!(
engine.eval::<INT>( engine.eval::<INT>(
r#" r#"
@ -73,11 +85,13 @@ fn test_function_pointers() -> Result<(), Box<EvalAltResult>> {
42 42
); );
#[cfg(not(feature = "no_object"))]
assert!(matches!( assert!(matches!(
*engine.eval::<INT>(r#"let f = Fn("abc"); f.call(0)"#).expect_err("should error"), *engine.eval::<INT>(r#"let f = Fn("abc"); f.call(0)"#).expect_err("should error"),
EvalAltResult::ErrorFunctionNotFound(f, _) if f.starts_with("abc (") EvalAltResult::ErrorFunctionNotFound(f, _) if f.starts_with("abc (")
)); ));
#[cfg(not(feature = "no_object"))]
assert_eq!( assert_eq!(
engine.eval::<INT>( engine.eval::<INT>(
r#" r#"

View File

@ -173,17 +173,16 @@ fn test_string_fn() -> Result<(), Box<EvalAltResult>> {
"foo" "foo"
); );
engine.register_fn("foo1", |s: &str| s.len() as INT); engine
engine.register_fn("foo2", |s: ImmutableString| s.len() as INT); .register_fn("foo1", |s: &str| s.len() as INT)
engine.register_fn("foo3", |s: String| s.len() as INT); .register_fn("foo2", |s: ImmutableString| s.len() as INT)
.register_fn("foo3", |s: String| s.len() as INT)
.register_fn("foo4", |s: &mut ImmutableString| s.len() as INT);
assert_eq!(engine.eval::<INT>(r#"foo1("hello")"#)?, 5); assert_eq!(engine.eval::<INT>(r#"foo1("hello")"#)?, 5);
assert_eq!(engine.eval::<INT>(r#"foo2("hello")"#)?, 5); assert_eq!(engine.eval::<INT>(r#"foo2("hello")"#)?, 5);
assert_eq!(engine.eval::<INT>(r#"foo3("hello")"#)?, 5);
assert!(matches!( assert_eq!(engine.eval::<INT>(r#"foo4("hello")"#)?, 5);
*engine.eval::<INT>(r#"foo3("hello")"#).expect_err("should error"),
EvalAltResult::ErrorFunctionNotFound(err, _) if err == "foo3 (&str | ImmutableString)"
));
Ok(()) Ok(())
} }

View File

@ -35,6 +35,7 @@ fn test_tokens_custom_operator() -> Result<(), Box<EvalAltResult>> {
15 15
); );
#[cfg(not(feature = "no_function"))]
assert_eq!( assert_eq!(
engine.eval::<INT>( engine.eval::<INT>(
r" r"