diff --git a/Cargo.toml b/Cargo.toml index 6d48bc8d..fe72ee21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rhai" -version = "0.17.0" +version = "0.18.0" edition = "2018" authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung"] description = "Embedded scripting for Rust" @@ -73,9 +73,3 @@ optional = true [target.'cfg(target_arch = "wasm32")'.dependencies] 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"] diff --git a/README.md b/README.md index 285595d0..1f9f5bf8 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ Supported targets and builds * WebAssembly (WASM) * `no-std` -Features --------- +Standard features +----------------- * 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). @@ -30,20 +30,30 @@ Features * Relatively little `unsafe` code (yes there are some for performance reasons, and most `unsafe` code is limited to one single source file, all with names starting with `"unsafe_"`). * Re-entrant scripting engine can be made `Send + Sync` (via the `sync` feature). -* Sand-boxed - the scripting engine, if declared immutable, cannot mutate the containing environment unless explicitly permitted (e.g. via a `RefCell`). -* Rugged - 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). * [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). * 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). * 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. * 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 ------------- diff --git a/RELEASES.md b/RELEASES.md index c1bbc632..9a48b709 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,15 +1,27 @@ 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 ============== This version adds: * [`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. +* Surgically disable keywords and/or operators in the language. +* Define custom operators. +* Extend the language via custom syntax. Bug fixes --------- @@ -22,7 +34,6 @@ Breaking changes * `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. * 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 ------------ @@ -31,6 +42,7 @@ New features 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::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 `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`. @@ -38,6 +50,8 @@ New features * `FnPtr` is exposed as the function pointer type. * `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). +* 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 diff --git a/doc/src/advanced.md b/doc/src/advanced.md index 3cb28cb5..b7d69e39 100644 --- a/doc/src/advanced.md +++ b/doc/src/advanced.md @@ -5,6 +5,12 @@ Advanced Topics 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. diff --git a/doc/src/context.json b/doc/src/context.json index 506b72e1..025c159b 100644 --- a/doc/src/context.json +++ b/doc/src/context.json @@ -1,5 +1,5 @@ { - "version": "0.17.0", + "version": "0.18.0", "rootUrl": "", "rootUrlX": "/rhai", "rootUrlXX": "/rhai/vnext" diff --git a/doc/src/engine/call-fn.md b/doc/src/engine/call-fn.md index 7a49d6e4..058d6dab 100644 --- a/doc/src/engine/call-fn.md +++ b/doc/src/engine/call-fn.md @@ -91,5 +91,5 @@ let result = engine.call_fn_dynamic( [ 41_i64.into() ] )?; -assert_eq!(value.as_int().unwrap(), 42); +assert_eq!(value.as_int()?, 42); ``` diff --git a/doc/src/engine/custom-op.md b/doc/src/engine/custom-op.md index 0b92c8a9..79f70d7c 100644 --- a/doc/src/engine/custom-op.md +++ b/doc/src/engine/custom-op.md @@ -21,7 +21,7 @@ let mut engine = Engine::new(); // (i.e. between +|- and *|/) // Also register the implementation of the customer operator as a function engine - .register_custom_operator("foo", 160).unwrap() + .register_custom_operator("foo", 160)? .register_fn("foo", |x: i64, y: i64| (x * y) - (x + y)); // The custom operator can be used in expressions @@ -72,7 +72,7 @@ _Unary_ custom operators are not supported. ```rust engine - .register_custom_operator("foo", 160).unwrap() + .register_custom_operator("foo", 160)? .register_fn("foo", |x: i64| x * x); engine.eval::("1 + 2 * 3 foo 4 - 5 / 6")?; // error: function 'foo (i64, i64)' not found diff --git a/doc/src/language/dynamic.md b/doc/src/language/dynamic.md index 9ebe7d6f..9a3c27b7 100644 --- a/doc/src/language/dynamic.md +++ b/doc/src/language/dynamic.md @@ -66,7 +66,7 @@ item.is::() == true; // 'is' returns whether a 'Dynam let value = item.cast::(); // if the element is 'i64', this succeeds; otherwise it panics let value: i64 = item.cast(); // type can also be inferred -let value = item.try_cast::().unwrap(); // 'try_cast' does not panic when the cast fails, but returns 'None' +let value = item.try_cast::()?; // 'try_cast' does not panic when the cast fails, but returns 'None' ``` Type Name diff --git a/doc/src/language/fn-ptr.md b/doc/src/language/fn-ptr.md index e4d9bc5d..57f2f24f 100644 --- a/doc/src/language/fn-ptr.md +++ b/doc/src/language/fn-ptr.md @@ -6,7 +6,7 @@ Function Pointers 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. -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 @@ -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 -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 @@ -66,20 +66,18 @@ Because of their dynamic nature, function pointers cannot refer to functions in See [function namespaces] for more details. ```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_something_now() { // call it from a local function +fn do_work_now() { // call it from a local function 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! ``` @@ -134,3 +132,35 @@ let func = sign(x) + 1; // Dynamic dispatch 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`]. diff --git a/doc/src/language/method.md b/doc/src/language/method.md index bdce0ed6..6b6b9314 100644 --- a/doc/src/language/method.md +++ b/doc/src/language/method.md @@ -3,6 +3,10 @@ Call Method as Function {{#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 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. @@ -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' ``` + + +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. diff --git a/doc/src/language/strings-chars.md b/doc/src/language/strings-chars.md index b6372240..acdb06f4 100644 --- a/doc/src/language/strings-chars.md +++ b/doc/src/language/strings-chars.md @@ -6,20 +6,25 @@ Strings and Characters String in Rhai contain any text sequence of valid Unicode characters. 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] -but excluded if using a [raw `Engine`]). This is particularly useful when printing output. +Strings can be built up from other strings and types via the `+` operator +(provided by the [`MoreStringPackage`][packages] but excluded if using a [raw `Engine`]). +This is particularly useful when printing output. [`type_of()`] a string returns `"string"`. The maximum allowed length of a string can be controlled via `Engine::set_max_string_size` (see [maximum length of strings]). + The `ImmutableString` Type ------------------------- 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 @@ -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. -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 ---------------- -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. diff --git a/doc/src/rust/getters-setters.md b/doc/src/rust/getters-setters.md index b6afd6f0..4c44399d 100644 --- a/doc/src/rust/getters-setters.md +++ b/doc/src/rust/getters-setters.md @@ -5,6 +5,8 @@ Custom Type Getters and Setters 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 #[derive(Clone)] struct TestStruct { diff --git a/doc/src/rust/indexers.md b/doc/src/rust/indexers.md index 35f50ba2..d5905b44 100644 --- a/doc/src/rust/indexers.md +++ b/doc/src/rust/indexers.md @@ -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. +Like getters and setters, indexers take a `&mut` reference to the first parameter. + 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 @@ -33,12 +35,10 @@ impl TestStruct { let mut engine = Engine::new(); -engine.register_type::(); - -engine.register_fn("new_ts", TestStruct::new); - -// Shorthand: engine.register_indexer_get_set(TestStruct::get_field, TestStruct::set_field); engine + .register_type::() + .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_set(TestStruct::set_field); diff --git a/doc/src/rust/strings.md b/doc/src/rust/strings.md index 5b72fe6e..0d54d87b 100644 --- a/doc/src/rust/strings.md +++ b/doc/src/rust/strings.md @@ -3,11 +3,19 @@ {{#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 -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_len3(s: ImmutableString) -> i64 { s.len() as i64 } // <- the above is equivalent to this @@ -20,3 +28,23 @@ let len = engine.eval::("x.len1()")?; // error: function ' let len = engine.eval::("x.len2()")?; // works fine let len = engine.eval::("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. diff --git a/doc/src/safety/max-array-size.md b/doc/src/safety/max-array-size.md index af5ba6f0..d2724e0f 100644 --- a/doc/src/safety/max-array-size.md +++ b/doc/src/safety/max-array-size.md @@ -3,8 +3,8 @@ Maximum Size of Arrays {{#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. diff --git a/doc/src/safety/max-call-stack.md b/doc/src/safety/max-call-stack.md index f10a8fbe..2d33d653 100644 --- a/doc/src/safety/max-call-stack.md +++ b/doc/src/safety/max-call-stack.md @@ -3,8 +3,8 @@ Maximum Call Stack Depth {{#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). diff --git a/doc/src/safety/max-map-size.md b/doc/src/safety/max-map-size.md index 2fcd3273..a980562b 100644 --- a/doc/src/safety/max-map-size.md +++ b/doc/src/safety/max-map-size.md @@ -3,8 +3,8 @@ Maximum Size of Object Maps {{#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. diff --git a/doc/src/safety/max-operations.md b/doc/src/safety/max-operations.md index 6dd5d7d9..e4778b88 100644 --- a/doc/src/safety/max-operations.md +++ b/doc/src/safety/max-operations.md @@ -3,8 +3,8 @@ Maximum Number of Operations {{#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. diff --git a/doc/src/safety/max-stmt-depth.md b/doc/src/safety/max-stmt-depth.md index e8b5ee84..e9749689 100644 --- a/doc/src/safety/max-stmt-depth.md +++ b/doc/src/safety/max-stmt-depth.md @@ -3,8 +3,8 @@ Maximum Statement Depth {{#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 (which should be plenty) when they are at _global_ level, but only a depth of 32 diff --git a/doc/src/safety/max-string-size.md b/doc/src/safety/max-string-size.md index 4b279d68..c693af9c 100644 --- a/doc/src/safety/max-string-size.md +++ b/doc/src/safety/max-string-size.md @@ -3,8 +3,8 @@ Maximum Length of Strings {{#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. diff --git a/doc/src/safety/progress.md b/doc/src/safety/progress.md index 00c2ed0e..3432a00e 100644 --- a/doc/src/safety/progress.md +++ b/doc/src/safety/progress.md @@ -1,5 +1,5 @@ -Tracking Progress and Force-Termination -====================================== +Track Progress and Force-Termination +=================================== {{#include ../links.md}} diff --git a/doc/src/start/builds/performance.md b/doc/src/start/builds/performance.md index 5db1edc1..dc47a4cc 100644 --- a/doc/src/start/builds/performance.md +++ b/doc/src/start/builds/performance.md @@ -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`. 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. diff --git a/doc/src/start/examples/rust.md b/doc/src/start/examples/rust.md index 412589e5..b7b6a5a5 100644 --- a/doc/src/start/examples/rust.md +++ b/doc/src/start/examples/rust.md @@ -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. | | [`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. | -| [`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.
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`]. | | [`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).
The [`serde`] feature is required to run. | diff --git a/src/any.rs b/src/any.rs index bd21404b..5182da08 100644 --- a/src/any.rs +++ b/src/any.rs @@ -212,9 +212,11 @@ pub(crate) fn map_std_type_name(name: &str) -> &str { "string" } else if name == type_name::() { "Fn" - } else if name == type_name::() { - "timestamp" } else { + #[cfg(not(feature = "no_std"))] + if name == type_name::() { + return "timestamp"; + } #[cfg(not(feature = "no_index"))] if name == type_name::() { return "array"; diff --git a/src/engine.rs b/src/engine.rs index c266b75e..0b7ac58e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -26,7 +26,7 @@ use crate::stdlib::{ boxed::Box, collections::{HashMap, HashSet}, convert::TryFrom, - format, + fmt, format, iter::{empty, once}, mem, string::{String, ToString}, @@ -96,6 +96,7 @@ pub const MARKER_BLOCK: &str = "$block$"; pub const MARKER_IDENT: &str = "$ident$"; #[cfg(feature = "internals")] +#[derive(Debug, Clone, Hash)] pub struct Expression<'a>(&'a Expr); #[cfg(feature = "internals")] @@ -346,6 +347,15 @@ pub struct Engine { 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 { fn default() -> Self { // Create the new scripting Engine @@ -855,7 +865,7 @@ impl Engine { fn_name, args.iter() .map(|name| if name.is::() { - "&str | ImmutableString" + "&str | ImmutableString | String" } else { self.map_type_name((*name).type_name()) }) @@ -934,7 +944,7 @@ impl Engine { } // 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 // First check script-defined functions @@ -976,13 +986,15 @@ impl Engine { match fn_name { // type_of - KEYWORD_TYPE_OF if args.len() == 1 && !self.has_override(lib, hashes) => Ok(( - self.map_type_name(args[0].type_name()).to_string().into(), - false, - )), + KEYWORD_TYPE_OF if args.len() == 1 && !self.has_override(lib, hashes.0, hashes.1) => { + Ok(( + self.map_type_name(args[0].type_name()).to_string().into(), + false, + )) + } // 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( "'Fn' should not be called in method style. Try Fn(...);".into(), Position::none(), @@ -990,7 +1002,7 @@ impl Engine { } // 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( "'eval' should not be called in method style. Try eval(...);".into(), Position::none(), @@ -1076,10 +1088,10 @@ impl Engine { let idx = idx_val.downcast_mut::>().unwrap(); 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 call // Redirect function name - fn_name = obj.as_str().unwrap(); + let fn_name = obj.as_str().unwrap(); // Recalculate hash let hash = calc_fn_hash(empty(), fn_name, idx.len(), empty()); // Arguments are passed as-is @@ -1090,11 +1102,32 @@ impl Engine { self.exec_fn_call( 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 call on object + // Redirect function name + let fn_name = idx[0] + .downcast_ref::() + .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::>(); + 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 { let redirected: Option; let mut hash = *hash; // Check if it is a map method call in OOP style + #[cfg(not(feature = "no_object"))] if let Some(map) = obj.downcast_ref::() { if let Some(val) = map.get(fn_name) { if let Some(f) = val.downcast_ref::() { @@ -1915,7 +1948,7 @@ impl Engine { let hash_fn = calc_fn_hash(empty(), name, 1, once(TypeId::of::())); - if !self.has_override(lib, (hash_fn, *hash)) { + if !self.has_override(lib, hash_fn, *hash) { // Fn - only in function call style let expr = args_expr.get(0); let arg_value = @@ -1941,7 +1974,7 @@ impl Engine { let hash_fn = calc_fn_hash(empty(), name, 1, once(TypeId::of::())); - if !self.has_override(lib, (hash_fn, *hash)) { + if !self.has_override(lib, hash_fn, *hash) { // eval - only in function call style let prev_len = scope.len(); 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::() { + // Redirect function name + redirected = Some(fn_ptr.cast::().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::()).into(), + fn_ptr.type_name().into(), + expr.position(), + ))); + } + } + // Normal function call - except for Fn and eval (handled above) let mut arg_values: StaticVec; let mut args: StaticVec<_>; @@ -1972,7 +2035,7 @@ impl Engine { } else { // 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 - match args_expr.get(0) { + match args_expr.get(0).unwrap() { // func(x, ...) -> x.func(...) lhs @ Expr::Variable(_) => { arg_values = args_expr @@ -2009,7 +2072,7 @@ impl Engine { let args = args.as_mut(); 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_err(|err| err.new_position(*pos)) diff --git a/src/error.rs b/src/error.rs index 6b3308ba..1f4b08bf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -98,6 +98,8 @@ pub enum ParseErrorType { PropertyExpected, /// Missing a variable name after the `let`, `const` or `for` keywords. VariableExpected, + /// An identifier is a reserved keyword. + Reserved(String), /// Missing an expression. Wrapped value is the expression type. ExprExpected(String), /// 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::PropertyExpected => "Expecting name of a property", Self::VariableExpected => "Expecting name of a variable", + Self::Reserved(_) => "Invalid use of reserved keyword", Self::ExprExpected(_) => "Expecting an expression", Self::FnMissingName => "Expecting name in function declaration", Self::FnMissingParams(_) => "Expecting parameters in function declaration", @@ -224,6 +227,7 @@ impl fmt::Display for ParseErrorType { Self::LiteralTooLarge(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()), } } diff --git a/src/fn_native.rs b/src/fn_native.rs index c7b2a4c9..2f36a27d 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -8,7 +8,7 @@ use crate::result::EvalAltResult; use crate::token::{is_valid_identifier, Position}; 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. #[cfg(feature = "sync")] @@ -87,7 +87,7 @@ impl TryFrom for FnPtr { Ok(Self(value)) } else { Err(Box::new(EvalAltResult::ErrorFunctionNotFound( - value.to_string(), + value.into(), Position::none(), ))) } diff --git a/src/fn_register.rs b/src/fn_register.rs index e516216c..4ad5b36a 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -7,10 +7,16 @@ use crate::fn_native::{CallableFunction, FnAny, FnCallArgs, SendSync}; use crate::module::Module; use crate::parser::FnAccess; use crate::plugin::Plugin; +use crate::r#unsafe::unsafe_cast_box; use crate::result::EvalAltResult; 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`. /// @@ -185,6 +191,9 @@ pub fn by_value(data: &mut Dynamic) -> T { let ref_str = data.as_str().unwrap(); let ref_T = unsafe { mem::transmute::<_, &T>(&ref_str) }; ref_T.clone() + } else if TypeId::of::() == TypeId::of::() { + // 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 { // 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. @@ -240,13 +249,15 @@ pub fn map_result( data } -/// Remap `&str` to `ImmutableString`. +/// Remap `&str` | `String` to `ImmutableString`. #[inline(always)] fn map_type_id() -> TypeId { let id = TypeId::of::(); if id == TypeId::of::<&str>() { TypeId::of::() + } else if id == TypeId::of::() { + TypeId::of::() } else { id } diff --git a/src/module.rs b/src/module.rs index c64d16e2..5a6a604c 100644 --- a/src/module.rs +++ b/src/module.rs @@ -25,11 +25,13 @@ use crate::stdlib::{ num::NonZeroUsize, ops::{Deref, DerefMut}, string::{String, ToString}, - sync::RwLock, vec, vec::Vec, }; +#[cfg(not(feature = "no_std"))] +use crate::stdlib::sync::RwLock; + /// Return type of module-level Rust function. pub type FuncReturn = Result>; diff --git a/src/packages/mod.rs b/src/packages/mod.rs index d75e0adc..43ae5f28 100644 --- a/src/packages/mod.rs +++ b/src/packages/mod.rs @@ -52,7 +52,7 @@ pub type PackageLibrary = Shared; /// Type containing a collection of `PackageLibrary` instances. /// 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); impl PackagesCollection { diff --git a/src/parser.rs b/src/parser.rs index 55fbdabf..e2f0c3f2 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -7,7 +7,7 @@ use crate::error::{LexError, ParseError, ParseErrorType}; use crate::module::{Module, ModuleRef}; use crate::optimize::{optimize_into_ast, OptimizationLevel}; 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}; #[cfg(feature = "internals")] @@ -25,6 +25,7 @@ use crate::stdlib::{ char, collections::HashMap, fmt, format, + hash::Hash, iter::empty, mem, num::NonZeroUsize, @@ -340,7 +341,7 @@ impl fmt::Display for FnAccess { } /// A scripted function definition. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Hash)] pub struct ScriptFnDef { /// Function name. pub name: String, @@ -374,7 +375,7 @@ impl fmt::Display for ScriptFnDef { } /// `return`/`throw` statement. -#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] +#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] pub enum ReturnType { /// `return` statement. Return, @@ -440,6 +441,8 @@ struct ParseSettings { pos: Position, /// Is the construct being parsed located at global level? 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_breakable: bool, /// Is anonymous function allowed? @@ -476,7 +479,7 @@ impl ParseSettings { /// /// Each variant is at most one pointer in size (for speed), /// with everything being allocated together in one single tuple. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Hash)] pub enum Stmt { /// No-op. Noop(Position), @@ -588,6 +591,13 @@ impl fmt::Debug for CustomExpr { } } +#[cfg(feature = "internals")] +impl Hash for CustomExpr { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + /// An expression. /// /// 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(&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 { /// Get the `Dynamic` value of a constant expression. /// @@ -1343,56 +1365,57 @@ fn parse_map_literal( eat_token(input, Token::RightBrace); 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::::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::::into(name), pos), expr)); + match input.peek().unwrap() { (Token::Comma, _) => { eat_token(input, Token::Comma); @@ -1460,6 +1483,24 @@ fn parse_primary( let index = state.find_var(&s); 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())?, #[cfg(not(feature = "no_index"))] Token::LeftBracket => parse_array_literal(input, state, lib, settings.level_up())?, @@ -1471,7 +1512,7 @@ fn parse_primary( _ => { return Err( 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))) } + (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)), }, // Indexing @@ -1538,6 +1582,7 @@ fn parse_primary( _ => (), } + // Make sure identifiers are valid Ok(root_expr) } @@ -2094,6 +2139,9 @@ fn parse_expr( (Token::Identifier(s), pos) => { 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)), }, MARKER_EXPR => exprs.push(parse_expr(input, state, lib, settings)?), @@ -2261,10 +2309,12 @@ fn parse_for( let name = match input.next().unwrap() { // Variable name (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 (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 (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; @@ -2311,20 +2361,13 @@ fn parse_let( // let name ... let (name, pos) = match input.next().unwrap() { (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)), (_, 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 = ... if match_token(input, Token::Equals)? { // let name = expr @@ -2390,6 +2433,9 @@ fn parse_import( // import expr as name ... let (name, _) = match input.next().unwrap() { (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)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; @@ -2414,6 +2460,9 @@ fn parse_export( loop { let (id, id_pos) = match input.next().unwrap() { (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)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; @@ -2421,6 +2470,10 @@ fn parse_export( let rename = if match_token(input, Token::As)? { match input.next().unwrap() { (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)), } } else { @@ -2598,6 +2651,7 @@ fn parse_stmt( allow_stmt_expr: true, allow_anonymous_fn: true, is_global: false, + is_function_scope: true, is_breakable: false, level: 0, pos: pos, @@ -2695,7 +2749,11 @@ fn parse_fn( settings.ensure_level_within_max_limit(state.max_expr_depth)?; 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)), }; @@ -2790,6 +2848,7 @@ impl Engine { allow_stmt_expr: false, allow_anonymous_fn: false, is_global: true, + is_function_scope: false, is_breakable: false, level: 0, pos: Position::none(), @@ -2829,6 +2888,7 @@ impl Engine { allow_stmt_expr: true, allow_anonymous_fn: true, is_global: true, + is_function_scope: false, is_breakable: false, level: 0, pos: Position::none(), diff --git a/src/settings.rs b/src/settings.rs index e3265c4a..f1967cdf 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -4,6 +4,8 @@ use crate::optimize::OptimizationLevel; use crate::packages::PackageLibrary; use crate::token::is_valid_identifier; +use crate::stdlib::{boxed::Box, format, string::String}; + impl Engine { /// Load a new package into the `Engine`. /// diff --git a/src/stdlib.rs b/src/stdlib.rs index 64eda6fb..5ec6c2a0 100644 --- a/src/stdlib.rs +++ b/src/stdlib.rs @@ -16,7 +16,7 @@ mod inner { pub use core_error as error; pub mod collections { - pub use hashbrown::HashMap; + pub use hashbrown::{HashMap, HashSet}; } } diff --git a/src/syntax.rs b/src/syntax.rs index 62aa20bd..3b646d25 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -75,7 +75,7 @@ impl Engine { ) -> Result> + SendSync + 'static, - ) -> Result> { + ) -> Result<&mut Self, Box> { if value.is_empty() { return Err(Box::new(LexError::ImproperSymbol("".to_string()))); } diff --git a/src/token.rs b/src/token.rs index 460ddcb5..f3579317 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1,6 +1,10 @@ //! 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::parser::INT; use crate::utils::StaticVec; @@ -13,7 +17,7 @@ use crate::stdlib::{ boxed::Box, char, collections::HashMap, - fmt, + fmt, format, iter::Peekable, str::{Chars, FromStr}, string::{String, ToString}, @@ -275,8 +279,6 @@ impl Token { Or => "||", Ampersand => "&", And => "&&", - #[cfg(not(feature = "no_function"))] - Fn => "fn", Continue => "continue", Break => "break", Return => "return", @@ -297,8 +299,12 @@ impl Token { ModuloAssign => "%=", PowerOf => "~", PowerOfAssign => "~=", + + #[cfg(not(feature = "no_function"))] + Fn => "fn", #[cfg(not(feature = "no_function"))] Private => "private", + #[cfg(not(feature = "no_module"))] Import => "import", #[cfg(not(feature = "no_module"))] @@ -355,8 +361,6 @@ impl Token { "||" => Or, "&" => Ampersand, "&&" => And, - #[cfg(not(feature = "no_function"))] - "fn" => Fn, "continue" => Continue, "break" => Break, "return" => Return, @@ -377,17 +381,30 @@ impl Token { "%=" => ModuloAssign, "~" => PowerOf, "~=" => PowerOfAssign, + + #[cfg(not(feature = "no_function"))] + "fn" => Fn, #[cfg(not(feature = "no_function"))] "private" => Private, + #[cfg(not(feature = "no_module"))] "import" => Import, #[cfg(not(feature = "no_module"))] "export" => Export, #[cfg(not(feature = "no_module"))] "as" => As, + + #[cfg(feature = "no_function")] + "fn" | "private" => Reserved(syntax.into()), + + #[cfg(feature = "no_module")] + "import" | "export" | "as" => 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, }) @@ -1353,9 +1370,10 @@ impl<'a> Iterator for TokenIterator<'a, '_> { "'#' is not a valid symbol. Should it be '#{'?" .to_string(), ))), - token => Token::LexError(Box::new(LERR::ImproperSymbol( - format!("'{}' is not a valid symbol.", token) + token if !is_valid_identifier(token.chars()) => Token::LexError(Box::new(LERR::ImproperSymbol( + format!("'{}' is a reserved symbol.", token) ))), + _ => Token::Reserved(s) }, pos)), (r @ Some(_), None, None) => r, (Some((Token::Identifier(s), pos)), _, Some(custom)) if custom.contains_key(&s) => { diff --git a/src/utils.rs b/src/utils.rs index 5dd619ba..01fdd26b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -156,6 +156,12 @@ impl Drop for StaticVec { } } +impl Hash for StaticVec { + fn hash(&self, state: &mut H) { + self.iter().for_each(|x| x.hash(state)); + } +} + impl Default for StaticVec { fn default() -> Self { Self { diff --git a/tests/call_fn.rs b/tests/call_fn.rs index 62291830..a04bd614 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -115,7 +115,8 @@ fn test_anonymous_fn() -> Result<(), Box> { } #[test] -fn test_fn_ptr() -> Result<(), Box> { +#[cfg(not(feature = "no_object"))] +fn test_fn_ptr_raw() -> Result<(), Box> { let mut engine = Engine::new(); engine.register_raw_fn( diff --git a/tests/decrement.rs b/tests/decrement.rs index 1b74dfe9..0691eddf 100644 --- a/tests/decrement.rs +++ b/tests/decrement.rs @@ -8,7 +8,7 @@ fn test_decrement() -> Result<(), Box> { assert!(matches!( *engine.eval::(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(()) diff --git a/tests/fn_ptr.rs b/tests/fn_ptr.rs new file mode 100644 index 00000000..e9121475 --- /dev/null +++ b/tests/fn_ptr.rs @@ -0,0 +1,80 @@ +use rhai::{Engine, EvalAltResult, RegisterFn, INT}; + +#[test] +fn test_fn_ptr() -> Result<(), Box> { + 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::( + r#" + let f = Fn("bar"); + let x = 40; + f.call(x, 2); + x + "# + )?, + 40 + ); + + #[cfg(not(feature = "no_object"))] + assert_eq!( + engine.eval::( + r#" + let f = Fn("bar"); + let x = 40; + x.call(f, 2); + x + "# + )?, + 42 + ); + + assert_eq!( + engine.eval::( + 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::( + 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::( + 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(()) +} diff --git a/tests/functions.rs b/tests/functions.rs index ed3eeb73..3c0a080b 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -52,12 +52,24 @@ fn test_functions() -> Result<(), Box> { } #[test] -#[cfg(not(feature = "no_object"))] fn test_function_pointers() -> Result<(), Box> { let engine = Engine::new(); assert_eq!(engine.eval::(r#"type_of(Fn("abc"))"#)?, "Fn"); + assert_eq!( + engine.eval::( + r#" + fn foo(x) { 40 + x } + + let f = Fn("foo"); + call(f, 2) + "# + )?, + 42 + ); + + #[cfg(not(feature = "no_object"))] assert_eq!( engine.eval::( r#" @@ -73,11 +85,13 @@ fn test_function_pointers() -> Result<(), Box> { 42 ); + #[cfg(not(feature = "no_object"))] assert!(matches!( *engine.eval::(r#"let f = Fn("abc"); f.call(0)"#).expect_err("should error"), EvalAltResult::ErrorFunctionNotFound(f, _) if f.starts_with("abc (") )); + #[cfg(not(feature = "no_object"))] assert_eq!( engine.eval::( r#" diff --git a/tests/string.rs b/tests/string.rs index e6b77841..908b4c6c 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -173,17 +173,16 @@ fn test_string_fn() -> Result<(), Box> { "foo" ); - engine.register_fn("foo1", |s: &str| s.len() as INT); - engine.register_fn("foo2", |s: ImmutableString| s.len() as INT); - engine.register_fn("foo3", |s: String| s.len() as INT); + engine + .register_fn("foo1", |s: &str| 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::(r#"foo1("hello")"#)?, 5); assert_eq!(engine.eval::(r#"foo2("hello")"#)?, 5); - - assert!(matches!( - *engine.eval::(r#"foo3("hello")"#).expect_err("should error"), - EvalAltResult::ErrorFunctionNotFound(err, _) if err == "foo3 (&str | ImmutableString)" - )); + assert_eq!(engine.eval::(r#"foo3("hello")"#)?, 5); + assert_eq!(engine.eval::(r#"foo4("hello")"#)?, 5); Ok(()) } diff --git a/tests/tokens.rs b/tests/tokens.rs index bb033619..f54afa84 100644 --- a/tests/tokens.rs +++ b/tests/tokens.rs @@ -35,6 +35,7 @@ fn test_tokens_custom_operator() -> Result<(), Box> { 15 ); + #[cfg(not(feature = "no_function"))] assert_eq!( engine.eval::( r"