diff --git a/Cargo.toml b/Cargo.toml index 23f4aa90..1f38d142 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rhai" -version = "0.15.0" +version = "0.15.1" edition = "2018" authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung"] description = "Embedded scripting for Rust" @@ -22,6 +22,7 @@ num-traits = { version = "0.2.11", default-features = false } [features] #default = ["unchecked", "sync", "no_optimize", "no_float", "only_i32", "no_index", "no_object", "no_function", "no_module"] default = [] +plugins = [] unchecked = [] # unchecked arithmetic sync = [] # restrict to only types that implement Send + Sync no_optimize = [] # no script optimizer @@ -62,3 +63,6 @@ version = "0.3.2" default-features = false features = ["compile-time-rng"] optional = true + +[target.'cfg(target_arch = "wasm32")'.dependencies] +instant= { version = "0.1.4", features = ["wasm-bindgen"] } # WASM implementation of std::time::Instant diff --git a/README.md b/README.md index f894ea7b..d79c1218 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,17 @@ Rhai - Embedded Scripting for Rust Rhai is an embedded scripting language and evaluation engine for Rust that gives a safe and easy way to add scripting to any application. +Supported targets +----------------- + +* All common CPU targets for Windows, Linux and MacOS. +* [WASM] +* `no-std` + Features -------- -* Easy-to-use language similar to JS+Rust with dynamic typing but _no_ garbage collector. +* Easy-to-use language similar to JS+Rust with dynamic typing. * Tight integration with native Rust [functions](#working-with-functions) and [types](#custom-types-and-methods), including [getters/setters](#getters-and-setters), [methods](#members-and-methods) and [indexers](#indexers). * Freely pass Rust variables/constants into a script via an external [`Scope`]. @@ -25,19 +32,18 @@ Features one single source file, all with names starting with `"unsafe_"`). * Re-entrant scripting [`Engine`] can be made `Send + Sync` (via the [`sync`] feature). * Sand-boxed - the scripting [`Engine`], if declared immutable, cannot mutate the containing environment unless explicitly permitted (e.g. via a `RefCell`). -* Rugged (protection against [stack-overflow](#maximum-call-stack-depth) and [runaway scripts](#maximum-number-of-operations) etc.). -* Track script evaluation [progress](#tracking-progress) and manually terminate a script run. -* [`no-std`](#optional-features) support. +* Rugged - protection against malicious attacks (such as [stack-overflow](#maximum-call-stack-depth), [over-sized data](#maximum-length-of-strings), and [runaway scripts](#maximum-number-of-operations) etc.) that may come from untrusted third-party user-land scripts. +* Track script evaluation [progress](#tracking-progress-and-force-terminate-script-run) and manually terminate a script run. * [Function overloading](#function-overloading). * [Operator overloading](#operator-overloading). -* Organize code base with dynamically-loadable [Modules]. +* Organize code base with dynamically-loadable [modules]. * Scripts are [optimized](#script-optimization) (useful for template-based machine-generated scripts) for repeated evaluations. -* Support for [minimal builds](#minimal-builds) by excluding unneeded language [features](#optional-features). +* Support for [minimal builds] by excluding unneeded language [features](#optional-features). * Very few additional dependencies (right now only [`num-traits`](https://crates.io/crates/num-traits/) to do checked arithmetic operations); for [`no-std`](#optional-features) builds, a number of additional dependencies are pulled in to provide for functionalities that used to be in `std`. -**Note:** Currently, the version is `0.15.0`, so the language and API's may change before they stabilize. +**Note:** Currently, the version is `0.15.1`, so the language and API's may change before they stabilize. What Rhai doesn't do -------------------- @@ -71,7 +77,7 @@ Install the Rhai crate on [`crates.io`](https::/crates.io/crates/rhai/) by addin ```toml [dependencies] -rhai = "0.15.0" +rhai = "0.15.1" ``` Use the latest released crate version on [`crates.io`](https::/crates.io/crates/rhai/): @@ -105,7 +111,7 @@ Optional features | `no_index` | Disable [arrays] and indexing features. | | `no_object` | Disable support for custom types and [object maps]. | | `no_function` | Disable script-defined functions. | -| `no_module` | Disable loading modules. | +| `no_module` | Disable loading external modules. | | `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | By default, Rhai includes all the standard functionalities in a small, tight package. @@ -141,8 +147,10 @@ Making [`Dynamic`] small helps performance due to better cache efficiency. ### Minimal builds -In order to compile a _minimal_build - i.e. a build optimized for size - perhaps for embedded targets, it is essential that -the correct linker flags are used in `cargo.toml`: +[minimal builds]: #minimal-builds + +In order to compile a _minimal_build - i.e. a build optimized for size - perhaps for `no-std` embedded targets or for +compiling to [WASM], it is essential that the correct linker flags are used in `cargo.toml`: ```toml [profile.release] @@ -156,7 +164,9 @@ all code is compiled in as what a script requires cannot be predicted. If a lang omitting them via special features is a prudent strategy to optimize the build for size. Omitting arrays (`no_index`) yields the most code-size savings, followed by floating-point support -(`no_float`), checked arithmetic (`unchecked`) and finally object maps and custom types (`no_object`). +(`no_float`), checked arithmetic/script resource limits (`unchecked`) and finally object maps and custom types (`no_object`). + +Where the usage scenario does not call for loading externally-defined modules, use `no_module` to save some bytes. Disable script-defined functions (`no_function`) only when the feature is not needed because code size savings is minimal. [`Engine::new_raw`](#raw-engine) creates a _raw_ engine. @@ -164,8 +174,26 @@ A _raw_ engine supports, out of the box, only a very [restricted set](#built-in- Selectively include other necessary functionalities by loading specific [packages] to minimize the footprint. Packages are sharable (even across threads via the [`sync`] feature), so they only have to be created once. -Related -------- +### Building to WebAssembly (WASM) + +[WASM]: #building-to-WebAssembly-wasm + +It is possible to use Rhai when compiling to WebAssembly (WASM). This yields a scripting engine (and language) +that can be run in a standard web browser. Why you would want to is another matter... as there is already +a nice, fast, complete scripting language for the the common WASM environment (i.e. a browser) - and it is called JavaScript. +But anyhow, do it because you _can_! + +When building for WASM, certain features will not be available, such as the script file API's and loading modules +from external script files. + +Also look into [minimal builds] to reduce generated WASM size. As of this version, a typical, full-featured +Rhai scripting engine compiles to a single WASM file less than 200KB gzipped. When excluding features that are +marginal in WASM environment, the gzipped payload can be further shrunk to 160KB. + +In benchmark tests, a WASM build runs scripts roughly 1.7-2.2x slower than a native optimized release build. + +Related Resources +----------------- Other cool projects to check out: @@ -179,13 +207,14 @@ A number of examples can be found in the `examples` folder: | Example | Description | | ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| [`arrays_and_structs`](examples/arrays_and_structs.rs) | demonstrates registering a new type to Rhai and the usage of [arrays] on it | -| [`custom_types_and_methods`](examples/custom_types_and_methods.rs) | shows how to register a type and methods for it | +| [`arrays_and_structs`](examples/arrays_and_structs.rs) | shows how to register a custom Rust type and using [arrays] on it | +| [`custom_types_and_methods`](examples/custom_types_and_methods.rs) | shows how to register a custom Rust type and methods for it | | [`hello`](examples/hello.rs) | simple example that evaluates an expression and prints the result | | [`no_std`](examples/no_std.rs) | example to test out `no-std` builds | | [`reuse_scope`](examples/reuse_scope.rs) | evaluates two pieces of code in separate runs, but using a common [`Scope`] | | [`rhai_runner`](examples/rhai_runner.rs) | runs each filename passed to it as a Rhai script | -| [`simple_fn`](examples/simple_fn.rs) | shows how to register a Rust function to a Rhai [`Engine`] | +| [`simple_fn`](examples/simple_fn.rs) | shows how to register a simple function | +| [`strings`](examples/strings.rs) | shows different ways to register functions taking string arguments | | [`repl`](examples/repl.rs) | a simple REPL, interactively evaluate statements from stdin | Examples can be run with the following command: @@ -314,8 +343,8 @@ Functions declared with `private` are hidden and cannot be called from Rust (see ```rust // Define functions in a script. let ast = engine.compile(true, - r" - // a function with two parameters: String and i64 + r#" + // a function with two parameters: string and i64 fn hello(x, y) { x.len + y } @@ -334,7 +363,7 @@ let ast = engine.compile(true, private hidden() { throw "you shouldn't see me!"; } - ")?; + "#)?; // A custom scope can also contain any variables/constants available to the functions let mut scope = Scope::new(); @@ -358,16 +387,14 @@ let result: i64 = engine.call_fn(&mut scope, &ast, "hello", () )?; let result: () = engine.call_fn(&mut scope, &ast, "hidden", ())?; ``` -For more control, construct all arguments as `Dynamic` values and use `Engine::call_fn_dynamic`: +For more control, construct all arguments as `Dynamic` values and use `Engine::call_fn_dynamic`, passing it +anything that implements `IntoIterator` (such as a simple `Vec`): ```rust let result: Dynamic = engine.call_fn_dynamic(&mut scope, &ast, "hello", - &mut [ String::from("abc").into(), 123_i64.into() ])?; + vec![ String::from("abc").into(), 123_i64.into() ])?; ``` -However, beware that `Engine::call_fn_dynamic` _consumes_ its arguments, meaning that all arguments passed to it -will be replaced by `()` afterwards. To re-use the arguments, clone them beforehand and pass in the clone. - ### Creating Rust anonymous functions from Rhai script [`Func`]: #creating-rust-anonymous-functions-from-rhai-script @@ -427,7 +454,7 @@ are supported. | `+`, | `+=` | `INT`, `FLOAT` (if not [`no_float`]), `ImmutableString` | | `-`, `*`, `/`, `%`, `~`, | `-=`, `*=`, `/=`, `%=`, `~=` | `INT`, `FLOAT` (if not [`no_float`]) | | `<<`, `>>`, `^`, | `<<=`, `>>=`, `^=` | `INT` | -| `&`, `\|`, | `&=`, `|=` | `INT`, `bool` | +| `&`, `\|`, | `&=`, `\|=` | `INT`, `bool` | | `&&`, `\|\|` | | `bool` | | `==`, `!=` | | `INT`, `FLOAT` (if not [`no_float`]), `bool`, `char`, `()`, `ImmutableString` | | `>`, `>=`, `<`, `<=` | | `INT`, `FLOAT` (if not [`no_float`]), `char`, `()`, `ImmutableString` | @@ -472,8 +499,7 @@ The follow packages are available: Packages typically contain Rust functions that are callable within a Rhai script. All functions registered in a package is loaded under the _global namespace_ (i.e. they're available without module qualifiers). Once a package is created (e.g. via `new`), it can be _shared_ (via `get`) among multiple instances of [`Engine`], -even across threads (if the [`sync`] feature is turned on). -Therefore, a package only has to be created _once_. +even across threads (under [`sync`]). Therefore, a package only has to be created _once_. Packages are actually implemented as [modules], so they share a lot of behavior and characteristics. The main difference is that a package loads under the _global_ namespace, while a module loads under its own @@ -496,7 +522,7 @@ In these cases, use the `compile_expression` and `eval_expression` methods or th let result = engine.eval_expression::("2 + (10 + 10) * 2")?; ``` -When evaluation _expressions_, no full-blown statement (e.g. `if`, `while`, `for`) - not even variable assignments - +When evaluating _expressions_, no full-blown statement (e.g. `if`, `while`, `for`) - not even variable assignments - is supported and will be considered parse errors when encountered. ```rust @@ -522,7 +548,7 @@ The following primitive types are supported natively: | **Floating-point number** (disabled with [`no_float`]) | `f32`, `f64` _(default)_ | `"f32"` or `"f64"` | `"123.4567"` etc. | | **Boolean value** | `bool` | `"bool"` | `"true"` or `"false"` | | **Unicode character** | `char` | `"char"` | `"A"`, `"x"` etc. | -| **Immutable Unicode string** | `rhai::ImmutableString` (implemented as `Rc` or `Arc`, _not_ `&str`) | `"string"` | `"hello"` etc. | +| **Immutable Unicode string** | `rhai::ImmutableString` (implemented as `Rc` or `Arc`) | `"string"` | `"hello"` etc. | | **Array** (disabled with [`no_index`]) | `rhai::Array` | `"array"` | `"[ ?, ?, ? ]"` | | **Object map** (disabled with [`no_object`]) | `rhai::Map` | `"map"` | `#{ "a": 1, "b": 2 }` | | **Timestamp** (implemented in the [`BasicTimePackage`](#packages)) | `std::time::Instant` | `"timestamp"` | _not supported_ | @@ -573,7 +599,7 @@ if type_of(x) == "string" { [`Dynamic`]: #dynamic-values -A `Dynamic` value can be _any_ type. However, if the [`sync`] feature is used, then all types must be `Send + Sync`. +A `Dynamic` value can be _any_ type. However, under [`sync`], all types must be `Send + Sync`. Because [`type_of()`] a `Dynamic` value returns the type of the actual value, it is usually used to perform type-specific actions based on the actual value's type. @@ -685,13 +711,18 @@ Rhai's scripting engine is very lightweight. It gets most of its abilities from To call these functions, they need to be registered with the [`Engine`]. ```rust -use rhai::{Dynamic, Engine, EvalAltResult}; +use rhai::{Dynamic, Engine, EvalAltResult, ImmutableString}; use rhai::RegisterFn; // use 'RegisterFn' trait for 'register_fn' use rhai::RegisterResultFn; // use 'RegisterResultFn' trait for 'register_result_fn' -// Normal function that returns any value type -fn add(x: i64, y: i64) -> i64 { - x + y +// Normal function that returns a standard type +// Remember to use 'ImmutableString' and not 'String' +fn add_len(x: i64, s: ImmutableString) -> i64 { + x + s.len() +} +// Alternatively, '&str' maps directly to 'ImmutableString' +fn add_len_str(x: i64, s: &str) -> i64 { + x + s.len() } // Function that returns a 'Dynamic' value - must return a 'Result' @@ -703,9 +734,14 @@ fn main() -> Result<(), Box> { let engine = Engine::new(); - engine.register_fn("add", add); + engine.register_fn("add", add_len); + engine.register_fn("add_str", add_len_str); - let result = engine.eval::("add(40, 2)")?; + let result = engine.eval::(r#"add(40, "xx")"#)?; + + println!("Answer: {}", result); // prints 42 + + let result = engine.eval::(r#"add_str(40, "xx")"#)?; println!("Answer: {}", result); // prints 42 @@ -728,7 +764,7 @@ use rhai::Dynamic; let x = (42_i64).into(); // 'into()' works for standard types -let y = Dynamic::from(String::from("hello!")); // remember &str is not supported by Rhai +let y = Dynamic::from("hello!".to_string()); // remember &str is not supported by Rhai ``` Functions registered with the [`Engine`] can be _overloaded_ as long as the _signature_ is unique, @@ -736,6 +772,25 @@ i.e. different functions can have the same name as long as their parameters are and/or different number. New definitions _overwrite_ previous definitions of the same name and same number/types of parameters. +### `String` parameters + +Functions accepting a parameter of `String` should use `&str` instead because it maps directly to `ImmutableString` +which is the type that Rhai uses to represent strings internally. + +```rust +fn get_len1(s: String) -> i64 { s.len() as i64 } // <- Rhai will not find this function +fn get_len2(s: &str) -> i64 { s.len() as i64 } // <- Rhai finds this function fine +fn get_len3(s: ImmutableString) -> i64 { s.len() as i64 } // <- the above is equivalent to this + +engine.register_fn("len1", get_len1); +engine.register_fn("len2", get_len2); +engine.register_fn("len3", get_len3); + +let len = engine.eval::("x.len1()")?; // error: function 'len1 (string)' not found +let len = engine.eval::("x.len2()")?; // works fine +let len = engine.eval::("x.len3()")?; // works fine +``` + Generic functions ----------------- @@ -976,8 +1031,8 @@ let result = engine.eval::( println!("result: {}", result); // prints 1 ``` -If the [`no_object`] feature is turned on, however, the _method_ style of function calls -(i.e. calling a function as an object-method) is no longer supported. +Under [`no_object`], however, the _method_ style of function calls (i.e. calling a function as an object-method) +is no longer supported. ```rust // Below is a syntax error under 'no_object' because 'clear' cannot be called in method style. @@ -1000,8 +1055,7 @@ let x = new_ts(); print(x.type_of()); // prints "Hello" ``` -Getters and setters -------------------- +### Getters and setters Similarly, custom types can expose members by registering a `get` and/or `set` function. @@ -1011,13 +1065,13 @@ struct TestStruct { field: String } -// Remember Rhai uses 'ImmutableString' instead of 'String' impl TestStruct { - fn get_field(&mut self) -> ImmutableString { - // Make an 'ImmutableString' from a 'String' - self.field.into(0) + // Returning a 'String' is OK - Rhai converts it into 'ImmutableString' + fn get_field(&mut self) -> String { + self.field.clone() } + // Remember Rhai uses 'ImmutableString' or '&str' instead of 'String' fn set_field(&mut self, new_val: ImmutableString) { // Get a 'String' from an 'ImmutableString' self.field = (*new_val).clone(); @@ -1041,12 +1095,10 @@ let result = engine.eval::(r#"let a = new_ts(); a.xyz = "42"; a.xyz"#)?; println!("Answer: {}", result); // prints 42 ``` -Indexers --------- +### Indexers Custom types can also expose an _indexer_ by registering an indexer function. A custom type with an indexer function defined can use the bracket '`[]`' notation to get a property value -(but not update it - indexers are read-only). ```rust #[derive(Clone)] @@ -1058,9 +1110,12 @@ impl TestStruct { fn get_field(&mut self, index: i64) -> i64 { self.fields[index as usize] } + fn set_field(&mut self, index: i64, value: i64) { + self.fields[index as usize] = value + } fn new() -> Self { - TestStruct { fields: vec![1, 2, 42, 4, 5] } + TestStruct { fields: vec![1, 2, 3, 4, 5] } } } @@ -1069,16 +1124,40 @@ let engine = Engine::new(); engine.register_type::(); engine.register_fn("new_ts", TestStruct::new); -engine.register_indexer(TestStruct::get_field); -let result = engine.eval::("let a = new_ts(); a[2]")?; +// Shorthand: engine.register_indexer_get_set(TestStruct::get_field, TestStruct::set_field); +engine.register_indexer_get(TestStruct::get_field); +engine.register_indexer_set(TestStruct::set_field); + +let result = engine.eval::("let a = new_ts(); a[2] = 42; a[2]")?; println!("Answer: {}", result); // prints 42 ``` -Needless to say, `register_type`, `register_type_with_name`, `register_get`, `register_set`, `register_get_set` -and `register_indexer` are not available when the [`no_object`] feature is turned on. -`register_indexer` is also not available when the [`no_index`] feature is turned on. +For efficiency reasons, indexers **cannot** be used to overload (i.e. override) built-in indexing operations for +[arrays] and [object maps]. + +### Disabling custom types + +The custom types API `register_type`, `register_type_with_name`, `register_get`, `register_set`, `register_get_set`, +`register_indexer_get`, `register_indexer_set` and `register_indexer_get_set` are not available under [`no_object`]. + +The indexers API `register_indexer_get`, `register_indexer_set` and `register_indexer_get_set` are also +not available under [`no_index`]. + +### Printing for custom types + +To use custom types for `print` and `debug`, or convert its value into a [string], it is necessary that the following +functions be registered (assuming the custom type is `T : Display + Debug`): + +| Function | Signature | Typical implementation | Usage | +| ----------- | ------------------------------------------------ | ------------------------------------- | --------------------------------------------------------------------------------------- | +| `to_string` | `|s: &mut T| -> ImmutableString` | `s.to_string().into()` | Converts the custom type into a [string] | +| `print` | `|s: &mut T| -> ImmutableString` | `s.to_string().into()` | Converts the custom type into a [string] for the [`print`](#print-and-debug) statement | +| `debug` | `|s: &mut T| -> ImmutableString` | `format!("{:?}", s).into()` | Converts the custom type into a [string] for the [`debug`](#print-and-debug) statement | +| `+` | `|s1: ImmutableString, s: T| -> ImmutableString` | `s1 + s` | Append the custom type to another [string], for `print("Answer: " + type);` usage | +| `+` | `|s: T, s2: ImmutableString| -> ImmutableString` | `s.to_string().push_str(&s2).into();` | Append another [string] to the custom type, for `print(type + " is the answer");` usage | +| `+=` | `|s1: &mut ImmutableString, s: T|` | `s1 += s.to_string()` | Append the custom type to an existing [string], for `s += type;` usage | `Scope` - Initializing and maintaining state ------------------------------------------- @@ -1089,8 +1168,8 @@ By default, Rhai treats each [`Engine`] invocation as a fresh one, persisting on but no global state. This gives each evaluation a clean starting slate. In order to continue using the same global state from one invocation to the next, such a state must be manually created and passed in. -All `Scope` variables are [`Dynamic`], meaning they can store values of any type. If the [`sync`] feature is used, however, -then only types that are `Send + Sync` are supported, and the entire `Scope` itself will also be `Send + Sync`. +All `Scope` variables are [`Dynamic`], meaning they can store values of any type. Under [`sync`], however, +only types that are `Send + Sync` are supported, and the entire `Scope` itself will also be `Send + Sync`. This is extremely useful in multi-threaded applications. In this example, a global state object (a `Scope`) is created with a few initialized variables, then the same state is @@ -1140,13 +1219,16 @@ fn main() -> Result<(), Box> Engine configuration options --------------------------- -| Method | Description | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| `set_optimization_level` | Set the amount of script _optimizations_ performed. See [script optimization]. | -| `set_max_expr_depths` | Set the maximum nesting levels of an expression/statement. See [maximum statement depth](#maximum-statement-depth). | -| `set_max_call_levels` | Set the maximum number of function call levels (default 50) to avoid infinite recursion. See [maximum call stack depth](#maximum-call-stack-depth). | -| `set_max_operations` | Set the maximum number of _operations_ that a script is allowed to consume. See [maximum number of operations](#maximum-number-of-operations). | -| `set_max_modules` | Set the maximum number of [modules] that a script is allowed to load. See [maximum number of modules](#maximum-number-of-modules). | +| Method | Not available under | Description | +| ------------------------ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `set_optimization_level` | [`no_optimize`] | Set the amount of script _optimizations_ performed. See [script optimization]. | +| `set_max_expr_depths` | [`unchecked`] | Set the maximum nesting levels of an expression/statement. See [maximum statement depth](#maximum-statement-depth). | +| `set_max_call_levels` | [`unchecked`] | Set the maximum number of function call levels (default 50) to avoid infinite recursion. See [maximum call stack depth](#maximum-call-stack-depth). | +| `set_max_operations` | [`unchecked`] | Set the maximum number of _operations_ that a script is allowed to consume. See [maximum number of operations](#maximum-number-of-operations). | +| `set_max_modules` | [`unchecked`] | Set the maximum number of [modules] that a script is allowed to load. See [maximum number of modules](#maximum-number-of-modules). | +| `set_max_string_size` | [`unchecked`] | Set the maximum length (in UTF-8 bytes) for [strings]. See [maximum length of strings](#maximum-length-of-strings). | +| `set_max_array_size` | [`unchecked`], [`no_index`] | Set the maximum size for [arrays]. See [maximum size of arrays](#maximum-size-of-arrays). | +| `set_max_map_size` | [`unchecked`], [`no_object`] | Set the maximum number of properties for [object maps]. See [maximum size of object maps](#maximum-size-of-object-maps). | ------- @@ -1191,7 +1273,7 @@ The following are reserved keywords in Rhai: | `import`, `export`, `as` | Modules | [`no_module`] | Keywords cannot be the name of a [function] or [variable], unless the relevant exclusive feature is enabled. -For example, `fn` is a valid variable name if the [`no_function`] feature is used. +For example, `fn` is a valid variable name under [`no_function`]. Statements ---------- @@ -1365,6 +1447,9 @@ Strings and Chars [strings]: #strings-and-chars [char]: #strings-and-chars +All strings in Rhai are implemented as `ImmutableString` (see [standard types]). +`ImmutableString` should be used in place of the standard Rust type `String` when registering functions. + String and character literals follow C-style formatting, with support for Unicode ('`\u`_xxxx_' or '`\U`_xxxxxxxx_') and hex ('`\x`_xx_') escape sequences. @@ -1442,8 +1527,16 @@ record == "Bob X. Davis: age 42 ❤\n"; "Davis" in record == true; 'X' in record == true; 'C' in record == false; + +// Strings can be iterated with a 'for' statement, yielding characters +for ch in record { + print(ch); +} ``` +The maximum allowed length of a string can be controlled via `Engine::set_max_string_size` +(see [maximum length of strings](#maximum-length-of-strings)). + ### Built-in functions The following standard methods (mostly defined in the [`MoreStringPackage`](#packages) but excluded if using a [raw `Engine`]) operate on strings: @@ -1542,6 +1635,8 @@ The following methods (mostly defined in the [`BasicArrayPackage`](#packages) bu ```rust let y = [2, 3]; // array literal with 2 elements +let y = [2, 3,]; // trailing comma is OK + y.insert(0, 1); // insert element at the beginning y.insert(999, 4); // insert element at the end @@ -1619,6 +1714,9 @@ y.len == 0; engine.register_fn("push", |list: &mut Array, item: MyType| list.push(Box::new(item)) ); ``` +The maximum allowed size of an array can be controlled via `Engine::set_max_array_size` +(see [maximum size of arrays](#maximum-size-of-arrays)). + Object maps ----------- @@ -1683,6 +1781,8 @@ ts.obj = y; // object maps can be assigned completely (by value copy let foo = ts.list.a; foo == 42; +let foo = #{ a:1,}; // trailing comma is OK + let foo = #{ a:1, b:2, c:3 }["a"]; foo == 1; @@ -1722,6 +1822,9 @@ y.clear(); // empty the object map y.len() == 0; ``` +The maximum allowed size of an object map can be controlled via `Engine::set_max_map_size` +(see [maximum size of object maps](#maximum-size-of-object-maps)). + ### Parsing from JSON The syntax for an object map is extremely similar to JSON, with the exception of `null` values which can @@ -1729,7 +1832,7 @@ technically be mapped to [`()`]. A valid JSON string does not start with a hash Rhai object map does - that's the major difference! JSON numbers are all floating-point while Rhai supports integers (`INT`) and floating-point (`FLOAT`) if -the [`no_float`] feature is not turned on. Most common generators of JSON data distinguish between +the [`no_float`] feature is not enabled. Most common generators of JSON data distinguish between integer and floating-point values by always serializing a floating-point number with a decimal point (i.e. `123.0` instead of `123` which is assumed to be an integer). This style can be used successfully with Rhai object maps. @@ -1945,9 +2048,18 @@ loop { Iterating through a range or an [array] is provided by the `for` ... `in` loop. ```rust -let array = [1, 3, 5, 7, 9, 42]; +// Iterate through string, yielding characters +let s = "hello, world!"; + +for ch in s { + if ch > 'z' { continue; } // skip to the next iteration + print(ch); + if x == '@' { break; } // break out of for loop +} // Iterate through array +let array = [1, 3, 5, 7, 9, 42]; + for x in array { if x > 10 { continue; } // skip to the next iteration print(x); @@ -2035,7 +2147,12 @@ fn add(x, y) { return x + y; } -print(add(2, 3)); +fn sub(x, y,) { // trailing comma in parameters list is OK + return x - y; +} + +print(add(2, 3)); // prints 5 +print(sub(2, 3,)); // prints -1 - trailing comma in arguments list is OK ``` ### Implicit return @@ -2141,7 +2258,7 @@ let a = new_ts(); // constructor function a.field = 500; // property setter a.update(); // method call, 'a' can be modified -update(a); // <- this de-sugars to 'a.update()' this if 'a' is a simple variable +update(a); // <- this de-sugars to 'a.update()' thus if 'a' is a simple variable // unlike scripted functions, 'a' can be modified and is not a copy let array = [ a ]; @@ -2361,10 +2478,10 @@ which simply loads a script file based on the path (with `.rhai` extension attac Built-in module resolvers are grouped under the `rhai::module_resolvers` module namespace. -| Module Resolver | Description | -| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `FileModuleResolver` | The default module resolution service, not available under the [`no_std`] feature. Loads a script file (based off the current directory) with `.rhai` extension.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | -| `StaticModuleResolver` | Loads modules that are statically added. This can be used when the [`no_std`] feature is turned on. | +| Module Resolver | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `FileModuleResolver` | The default module resolution service, not available under [`no_std`] or [WASM] builds. Loads a script file (based off the current directory) with `.rhai` extension.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | +| `StaticModuleResolver` | Loads modules that are statically added. This can be used under [`no_std`]. | An [`Engine`]'s module resolver is set via a call to `Engine::set_module_resolver`: @@ -2380,12 +2497,12 @@ engine.set_module_resolver(None); Ruggedization - protect against DoS attacks ------------------------------------------ -For scripting systems open to user-land scripts, it is always best to limit the amount of resources used by a script -so that it does not consume more resources that it is allowed to. +For scripting systems open to untrusted user-land scripts, it is always best to limit the amount of resources used by +a script so that it does not consume more resources that it is allowed to. The most important resources to watch out for are: -* **Memory**: A malicous script may continuously grow an [array] or [object map] until all memory is consumed. +* **Memory**: A malicous script may continuously grow a [string], an [array] or [object map] until all memory is consumed. It may also create a large [array] or [object map] literal that exhausts all memory during parsing. * **CPU**: A malicous script may run an infinite tight loop that consumes all CPU cycles. * **Time**: A malicous script may run indefinitely, thereby blocking the calling system which is waiting for a result. @@ -2401,6 +2518,85 @@ The most important resources to watch out for are: * **Data**: A malicous script may attempt to read from and/or write to data that it does not own. If this happens, it is a severe security breach and may put the entire system at risk. +### Maximum length of strings + +Rhai by default does not limit how long a [string] can be. +This can be changed via the `Engine::set_max_string_size` method, with zero being unlimited (the default). + +```rust +let mut engine = Engine::new(); + +engine.set_max_string_size(500); // allow strings only up to 500 bytes long (in UTF-8 format) + +engine.set_max_string_size(0); // allow unlimited string length +``` + +A script attempting to create a string literal longer than the maximum length will terminate with a parse error. +Any script operation that produces a string longer than the maximum also terminates the script with an error result. +This check can be disabled via the [`unchecked`] feature for higher performance +(but higher risks as well). + +Be conservative when setting a maximum limit and always consider the fact that a registered function may grow +a string's length without Rhai noticing until the very end. For instance, the built-in '`+`' operator for strings +concatenates two strings together to form one longer string; if both strings are _slightly_ below the maximum +length limit, the resultant string may be almost _twice_ the maximum length. + +### Maximum size of arrays + +Rhai by default does not limit how large an [array] can be. +This can be changed via the `Engine::set_max_array_size` method, with zero being unlimited (the default). + +```rust +let mut engine = Engine::new(); + +engine.set_max_array_size(500); // allow arrays only up to 500 items + +engine.set_max_array_size(0); // allow unlimited arrays +``` + +A script attempting to create an array literal larger than the maximum will terminate with a parse error. +Any script operation that produces an array larger than the maximum also terminates the script with an error result. +This check can be disabled via the [`unchecked`] feature for higher performance +(but higher risks as well). + +Be conservative when setting a maximum limit and always consider the fact that a registered function may grow +an array's size without Rhai noticing until the very end. +For instance, the built-in '`+`' operator for arrays concatenates two arrays together to form one larger array; +if both arrays are _slightly_ below the maximum size limit, the resultant array may be almost _twice_ the maximum size. + +As a malicious script may create a deeply-nested array which consumes huge amounts of memory while each individual +array still stays under the maximum size limit, Rhai also recursively adds up the sizes of all strings, arrays +and object maps contained within each array to make sure that the _aggregate_ sizes of none of these data structures +exceed their respective maximum size limits (if any). + +### Maximum size of object maps + +Rhai by default does not limit how large (i.e. the number of properties) an [object map] can be. +This can be changed via the `Engine::set_max_map_size` method, with zero being unlimited (the default). + +```rust +let mut engine = Engine::new(); + +engine.set_max_map_size(500); // allow object maps with only up to 500 properties + +engine.set_max_map_size(0); // allow unlimited object maps +``` + +A script attempting to create an object map literal with more properties than the maximum will terminate with a parse error. +Any script operation that produces an object map with more properties than the maximum also terminates the script with an error result. +This check can be disabled via the [`unchecked`] feature for higher performance +(but higher risks as well). + +Be conservative when setting a maximum limit and always consider the fact that a registered function may grow +an object map's size without Rhai noticing until the very end. For instance, the built-in '`+`' operator for object maps +concatenates two object maps together to form one larger object map; if both object maps are _slightly_ below the maximum +size limit, the resultant object map may be almost _twice_ the maximum size. + +As a malicious script may create a deeply-nested object map which consumes huge amounts of memory while each individual +object map still stays under the maximum size limit, Rhai also recursively adds up the sizes of all strings, arrays +and object maps contained within each object map to make sure that the _aggregate_ sizes of none of these data structures +exceed their respective maximum size limits (if any). + ### Maximum number of operations Rhai by default does not limit how much time or CPU a script consumes. @@ -2422,46 +2618,57 @@ A good rule-of-thumb is that one simple non-trivial expression consumes on avera One _operation_ can take an unspecified amount of time and real CPU cycles, depending on the particulars. For example, loading a constant consumes very few CPU cycles, while calling an external Rust function, though also counted as only one operation, may consume much more computing resources. -If it helps to visualize, think of an _operation_ as roughly equals to one _instruction_ of a hypothetical CPU. +To help visualize, think of an _operation_ as roughly equals to one _instruction_ of a hypothetical CPU +which includes _specialized_ instructions, such as _function call_, _load module_ etc., each taking up +one CPU cycle to execute. -The _operation count_ is intended to be a very course-grained measurement of the amount of CPU that a script -is consuming, and allows the system to impose a hard upper limit. +The _operations count_ is intended to be a very course-grained measurement of the amount of CPU that a script +has consumed, allowing the system to impose a hard upper limit on computing resources. -A script exceeding the maximum operations count will terminate with an error result. -This check can be disabled via the [`unchecked`] feature for higher performance -(but higher risks as well). +A script exceeding the maximum operations count terminates with an error result. +This can be disabled via the [`unchecked`] feature for higher performance (but higher risks as well). -### Tracking progress +### Tracking progress and force-terminate script run -To track script evaluation progress and to force-terminate a script prematurely (for any reason), -provide a closure to the `Engine::on_progress` method: +It is impossible to know when, or even whether, a script run will end +(a.k.a. the [Halting Problem](http://en.wikipedia.org/wiki/Halting_problem)). +When dealing with third-party untrusted scripts that may be malicious, to track evaluation progress and +to force-terminate a script prematurely (for any reason), provide a closure to the `Engine::on_progress` method: ```rust let mut engine = Engine::new(); -engine.on_progress(|count| { // 'count' is the number of operations performed +engine.on_progress(|&count| { // parameter is '&u64' - number of operations already performed if count % 1000 == 0 { println!("{}", count); // print out a progress log every 1,000 operations } - true // return 'true' to continue the script - // returning 'false' will terminate the script + true // return 'true' to continue running the script + // return 'false' to immediately terminate the script }); ``` -The closure passed to `Engine::on_progress` will be called once every operation. +The closure passed to `Engine::on_progress` will be called once for every operation. Return `false` to terminate the script immediately. +Notice that the _operations count_ value passed into the closure does not indicate the _percentage_ of work +already done by the script (and thus it is not real _progress_ tracking), because it is impossible to determine +how long a script may run. It is possible, however, to calculate this percentage based on an estimated +total number of operations for a typical run. + ### Maximum number of modules -Rhai by default does not limit how many [modules] are loaded via the [`import`] statement. -This can be changed via the `Engine::set_max_modules` method, with zero being unlimited (the default). +Rhai by default does not limit how many [modules] can be loaded via [`import`] statements. +This can be changed via the `Engine::set_max_modules` method. Notice that setting the maximum number +of modules to zero does _not_ indicate unlimited modules, but disallows loading any module altogether. ```rust let mut engine = Engine::new(); engine.set_max_modules(5); // allow loading only up to 5 modules -engine.set_max_modules(0); // allow unlimited modules +engine.set_max_modules(0); // disallow loading any module (maximum = zero) + +engine.set_max_modules(1000); // set to a large number for effectively unlimited modules ``` A script attempting to load more than the maximum number of modules will terminate with an error result. @@ -2474,7 +2681,7 @@ Rhai by default limits function calls to a maximum depth of 128 levels (16 level This limit may be changed via the `Engine::set_max_call_levels` method. When setting this limit, care must be also taken to the evaluation depth of each _statement_ -within the function. It is entirely possible for a malicous script to embed an recursive call deep +within the function. It is entirely possible for a malicous script to embed a recursive call deep inside a nested expression or statement block (see [maximum statement depth](#maximum-statement-depth)). The limit can be disabled via the [`unchecked`] feature for higher performance @@ -2573,7 +2780,7 @@ For example, in the following: ```rust { - let x = 999; // NOT eliminated: Rhai doesn't check yet whether a variable is used later on + let x = 999; // NOT eliminated: variable may be used later on (perhaps even an 'eval') 123; // eliminated: no effect "hello"; // eliminated: no effect [1, 2, x, x*2, 5]; // eliminated: no effect diff --git a/RELEASES.md b/RELEASES.md index 1208c720..4128b19b 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,9 +1,40 @@ Rhai Release Notes ================== -Version 0.14.2 +Version 0.15.1 ============== +This is a minor release which enables updating indexers (via registered indexer setters) and supports functions +with `&str` parameters (maps transparently to `ImmutableString`). WASM is also a tested target. + +Buf fix +------- + +* `let s="abc"; s[1].change_to('X');` now correctly sets the character '`X`' into '`s`' yielding `"aXc"`. + +Breaking changes +---------------- + +* Callback closure passed to `Engine::on_progress` now takes `&u64` instead of `u64` to be consistent with other callback signatures. +* `Engine::register_indexer` is renamed to `Engine::register_indexer_get`. +* `Module::set_indexer_fn` is renamed to `Module::set_indexer_get_fn`. +* The tuple `ParseError` now exposes the internal fields and the `ParseError::error_type` and `ParseError::position` methods are removed. The first tuple field is the `ParseErrorType` and the second tuple field is the `Position`. +* `Engine::call_fn_dynamic` now takes any type that implements `IntoIterator`. + +New features +------------ + +* Indexers are now split into getters and setters (which now support updates). The API is split into `Engine::register_indexer_get` and `Engine::register_indexer_set` with `Engine::register_indexer_get_set` being a shorthand. Similarly, `Module::set_indexer_get_fn` and `Module::set_indexer_set_fn` are added. +* `Engine:register_fn` and `Engine:register_result_fn` accepts functions that take parameters of type `&str` (immutable string slice), which maps directly to `ImmutableString`. This is to avoid needing wrappers for functions taking string parameters. +* Set maximum limit on data sizes: `Engine::set_max_string_size`, `Engine::set_max_array_size` and `Engine::set_max_map_size`. +* Supports trailing commas on array literals, object map literals, function definitions and function calls. +* Enhances support for compiling to WASM. + +Version 0.15.0 +============== + +This version uses immutable strings (`ImmutableString` type) and built-in operator functions (e.g. `+`, `>`, `+=`) to improve speed, plus some bug fixes. + Regression fix -------------- @@ -14,26 +45,18 @@ Bug fixes * Indexing with an index or dot expression now works property (it compiled wrongly before). For example, `let s = "hello"; s[s.len-1] = 'x';` now works property instead of causing a runtime error. +* `if` expressions are not supposed to be allowed when compiling for expressions only. This is fixed. Breaking changes ---------------- * `Engine::compile_XXX` functions now return `ParseError` instead of `Box`. -* The `RegisterDynamicFn` trait is merged into the `RegisterResultFn` trait which now always returns - `Result>`. +* The `RegisterDynamicFn` trait is merged into the `RegisterResultFn` trait which now always returns `Result>`. * Default maximum limit on levels of nested function calls is fine-tuned and set to a different value. -* Some operator functions are now built in (see _Speed enhancements_ below), so they are available even - under `Engine::new_raw`. -* Strings are now immutable. The type `rhai::ImmutableString` is used instead of `std::string::String`. - This is to avoid excessive cloning of strings. All native-Rust functions taking string parameters - should switch to `rhai::ImmutableString` (which is either `Rc` or `Arc` depending on - whether the `sync` feature is used). -* Native Rust functions registered with the `Engine` also mutates the first argument when called in - normal function-call style (previously the first argument will be passed by _value_ if not called - in method-call style). Of course, if the first argument is a calculated value (e.g. result of an - expression), then mutating it has no effect, but at least it is not cloned. -* Some built-in methods (e.g. `len` for string, `floor` for `FLOAT`) now have _property_ versions in - addition to methods to simplify coding. +* Some operator functions are now built in (see _Speed enhancements_ below), so they are available even under `Engine::new_raw`. +* Strings are now immutable. The type `rhai::ImmutableString` is used instead of `std::string::String`. This is to avoid excessive cloning of strings. All native-Rust functions taking string parameters should switch to `rhai::ImmutableString` (which is either `Rc` or `Arc` depending on whether the `sync` feature is used). +* Native Rust functions registered with the `Engine` also mutates the first argument when called in normal function-call style (previously the first argument will be passed by _value_ if not called in method-call style). Of course, if the first argument is a calculated value (e.g. result of an expression), then mutating it has no effect, but at least it is not cloned. +* Some built-in methods (e.g. `len` for string, `floor` for `FLOAT`) now have _property_ versions in addition to methods to simplify coding. New features ------------ @@ -46,23 +69,13 @@ New features Speed enhancements ------------------ -* Common operators (e.g. `+`, `>`, `==`) now call into highly efficient built-in implementations for standard types - (i.e. `INT`, `FLOAT`, `bool`, `char`, `()` and `ImmutableString`) if not overridden by a registered function. - This yields a 5-10% speed benefit depending on script operator usage. Scripts running tight loops will see - significant speed-up. -* Common assignment operators (e.g. `+=`, `%=`) now call into highly efficient built-in implementations for - standard types (i.e. `INT`, `FLOAT`, `bool`, `char`, `()` and `ImmutableString`) if not overridden by a registered function. -* Implementations of common operators for standard types are removed from the `ArithmeticPackage` and `LogicPackage` - (and therefore the `CorePackage`) because they are now always available, even under `Engine::new_raw`. +* Common operators (e.g. `+`, `>`, `==`) now call into highly efficient built-in implementations for standard types (i.e. `INT`, `FLOAT`, `bool`, `char`, `()` and `ImmutableString`) if not overridden by a registered function. This yields a 5-10% speed benefit depending on script operator usage. Scripts running tight loops will see significant speed-up. +* Common assignment operators (e.g. `+=`, `%=`) now call into highly efficient built-in implementations for standard types (i.e. `INT`, `FLOAT`, `bool`, `char`, `()` and `ImmutableString`) if not overridden by a registered function. +* Implementations of common operators for standard types are removed from the `ArithmeticPackage` and `LogicPackage` (and therefore the `CorePackage`) because they are now always available, even under `Engine::new_raw`. * Operator-assignment statements (e.g. `+=`) are now handled directly and much faster. * Strings are now _immutable_ and use the `rhai::ImmutableString` type, eliminating large amounts of cloning. -* For Native Rust functions taking a first `&mut` parameter, the first argument is passed by reference instead of - by value, even if not called in method-call style. This allows many functions declared with `&mut` parameter to avoid - excessive cloning. For example, if `a` is a large array, getting its length in this manner: `len(a)` used to result - in a full clone of `a` before taking the length and throwing the copy away. Now, `a` is simply passed by reference, - avoiding the cloning altogether. -* A custom hasher simply passes through `u64` keys without hashing to avoid function call hash keys - (which are by themselves `u64`) being hashed twice. +* For Native Rust functions taking a first `&mut` parameter, the first argument is passed by reference instead of by value, even if not called in method-call style. This allows many functions declared with `&mut` parameter to avoid excessive cloning. For example, if `a` is a large array, getting its length in this manner: `len(a)` used to result in a full clone of `a` before taking the length and throwing the copy away. Now, `a` is simply passed by reference, avoiding the cloning altogether. +* A custom hasher simply passes through `u64` keys without hashing to avoid function call hash keys (which are by themselves `u64`) being hashed twice. Version 0.14.1 @@ -74,15 +87,13 @@ The major features for this release is modules, script resource limits, and spee New features ------------ -* Modules and _module resolvers_ allow loading external scripts under a module namespace. - A module can contain constant variables, Rust functions and Rhai functions. +* Modules and _module resolvers_ allow loading external scripts under a module namespace. A module can contain constant variables, Rust functions and Rhai functions. * `export` variables and `private` functions. * _Indexers_ for Rust types. * Track script evaluation progress and terminate script run. * Set limit on maximum number of operations allowed per script run. * Set limit on maximum number of modules loaded per script run. -* A new API, `Engine::compile_scripts_with_scope`, can compile a list of script segments without needing to - first concatenate them together into one large string. +* A new API, `Engine::compile_scripts_with_scope`, can compile a list of script segments without needing to first concatenate them together into one large string. * Stepped `range` function with a custom step. Speed improvements diff --git a/_config.yml b/_config.yml deleted file mode 100644 index c7418817..00000000 --- a/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-slate \ No newline at end of file diff --git a/examples/repl.rs b/examples/repl.rs index f980a98e..f18a56e5 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -7,43 +7,41 @@ use std::io::{stdin, stdout, Write}; fn print_error(input: &str, err: EvalAltResult) { let lines: Vec<_> = input.trim().split('\n').collect(); + let pos = err.position(); let line_no = if lines.len() > 1 { - match err.position() { - p if p.is_none() => "".to_string(), - p => format!("{}: ", p.line().unwrap()), + if pos.is_none() { + "".to_string() + } else { + format!("{}: ", pos.line().unwrap()) } } else { "".to_string() }; // Print error - let pos = err.position(); let pos_text = format!(" ({})", pos); - match pos { - p if p.is_none() => { - // No position - println!("{}", err); - } - p => { - // Specific position - println!("{}{}", line_no, lines[p.line().unwrap() - 1]); + if pos.is_none() { + // No position + println!("{}", err); + } else { + // Specific position + println!("{}{}", line_no, lines[pos.line().unwrap() - 1]); - let err_text = match err { - EvalAltResult::ErrorRuntime(err, _) if !err.is_empty() => { - format!("Runtime error: {}", err) - } - err => err.to_string(), - }; + let err_text = match err { + EvalAltResult::ErrorRuntime(err, _) if !err.is_empty() => { + format!("Runtime error: {}", err) + } + err => err.to_string(), + }; - println!( - "{0:>1$} {2}", - "^", - line_no.len() + p.position().unwrap(), - err_text.replace(&pos_text, "") - ); - } + println!( + "{0:>1$} {2}", + "^", + line_no.len() + pos.position().unwrap(), + err_text.replace(&pos_text, "") + ); } } @@ -127,7 +125,7 @@ fn main() { match engine .compile_with_scope(&scope, &script) - .map_err(|err| err.into()) + .map_err(Into::into) .and_then(|r| { ast_u = r.clone(); diff --git a/examples/rhai_runner.rs b/examples/rhai_runner.rs index c5c5128d..135269ce 100644 --- a/examples/rhai_runner.rs +++ b/examples/rhai_runner.rs @@ -1,4 +1,4 @@ -use rhai::{Engine, EvalAltResult}; +use rhai::{Engine, EvalAltResult, Position}; #[cfg(not(feature = "no_optimize"))] use rhai::OptimizationLevel; @@ -6,15 +6,17 @@ use rhai::OptimizationLevel; use std::{env, fs::File, io::Read, process::exit}; fn eprint_error(input: &str, err: EvalAltResult) { - fn eprint_line(lines: &[&str], line: usize, pos: usize, err: &str) { + fn eprint_line(lines: &[&str], pos: Position, err: &str) { + let line = pos.line().unwrap(); + let line_no = format!("{}: ", line); - let pos_text = format!(" (line {}, position {})", line, pos); + let pos_text = format!(" ({})", pos); eprintln!("{}{}", line_no, lines[line - 1]); eprintln!( "{:>1$} {2}", "^", - line_no.len() + pos, + line_no.len() + pos.position().unwrap(), err.replace(&pos_text, "") ); eprintln!(""); @@ -25,22 +27,19 @@ fn eprint_error(input: &str, err: EvalAltResult) { // Print error let pos = err.position(); - match pos { - p if p.is_none() => { - // No position - eprintln!("{}", err); - } - p => { - // Specific position - let err_text = match err { - EvalAltResult::ErrorRuntime(err, _) if !err.is_empty() => { - format!("Runtime error: {}", err) - } - err => err.to_string(), - }; + if pos.is_none() { + // No position + eprintln!("{}", err); + } else { + // Specific position + let err_text = match err { + EvalAltResult::ErrorRuntime(err, _) if !err.is_empty() => { + format!("Runtime error: {}", err) + } + err => err.to_string(), + }; - eprint_line(&lines, p.line().unwrap(), p.position().unwrap(), &err_text) - } + eprint_line(&lines, pos, &err_text) } } diff --git a/examples/strings.rs b/examples/strings.rs new file mode 100644 index 00000000..e9d53016 --- /dev/null +++ b/examples/strings.rs @@ -0,0 +1,77 @@ +///! This example registers a variety of functions that operate on strings. +///! Remember to use `ImmutableString` or `&str` instead of `String` as parameters. +use rhai::{Engine, EvalAltResult, ImmutableString, RegisterFn, Scope, INT}; +use std::io::{stdin, stdout, Write}; + +/// Trim whitespace from a string. The original string argument is changed. +/// +/// This version uses `&mut ImmutableString` +fn trim_string(s: &mut ImmutableString) { + *s = s.trim().into(); +} + +/// Notice this is different from the built-in Rhai 'len' function for strings +/// which counts the actual number of Unicode _characters_ in a string. +/// This version simply counts the number of _bytes_ in the UTF-8 representation. +/// +/// This version uses `&str`. +fn count_string_bytes(s: &str) -> INT { + s.len() as INT +} + +/// This version uses `ImmutableString` and `&str`. +fn find_substring(s: ImmutableString, sub: &str) -> INT { + s.as_str().find(sub).map(|x| x as INT).unwrap_or(-1) +} + +fn main() -> Result<(), Box> { + // Create a `raw` Engine with no built-in string functions. + let mut engine = Engine::new_raw(); + + // Register string functions + engine.register_fn("trim", trim_string); + engine.register_fn("len", count_string_bytes); + engine.register_fn("index_of", find_substring); + + // Register string functions using closures + engine.register_fn("display", |label: &str, x: INT| { + println!("{}: {}", label, x) + }); + engine.register_fn("display", |label: ImmutableString, x: &str| { + println!(r#"{}: "{}""#, label, x) // Quote the input string + }); + + let mut scope = Scope::new(); + let mut input = String::new(); + + loop { + scope.clear(); + + println!("Type something. Press Ctrl-C to exit."); + print!("strings> "); + stdout().flush().expect("couldn't flush stdout"); + + input.clear(); + + if let Err(err) = stdin().read_line(&mut input) { + panic!("input error: {}", err); + } + + scope.push("x", input.clone()); + + println!("Line: {}", input.replace('\r', "\\r").replace('\n', "\\n")); + + engine.consume_with_scope( + &mut scope, + r#" + display("Length", x.len()); + x.trim(); + display("Trimmed", x); + display("Trimmed Length", x.len()); + display("Index of \"!!!\"", x.index_of("!!!")); + "#, + )?; + + println!(); + } +} diff --git a/src/any.rs b/src/any.rs index f685c303..6a5d9050 100644 --- a/src/any.rs +++ b/src/any.rs @@ -1,11 +1,10 @@ //! Helper module which defines the `Any` trait to to allow dynamic value handling. +use crate::fn_native::SendSync; +use crate::module::Module; use crate::parser::{ImmutableString, INT}; use crate::r#unsafe::{unsafe_cast_box, unsafe_try_cast}; -#[cfg(not(feature = "no_module"))] -use crate::module::Module; - #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; @@ -25,8 +24,13 @@ use crate::stdlib::{ }; #[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] use crate::stdlib::time::Instant; +#[cfg(not(feature = "no_std"))] +#[cfg(target_arch = "wasm32")] +use instant::Instant; + /// Trait to represent any type. /// /// Currently, `Variant` is not `Send` nor `Sync`, so it can practically be any type. @@ -56,31 +60,6 @@ pub trait Variant: Any { fn _closed(&self) -> _Private; } -#[cfg(not(feature = "sync"))] -impl Variant for T { - fn as_any(&self) -> &dyn Any { - self as &dyn Any - } - fn as_mut_any(&mut self) -> &mut dyn Any { - self as &mut dyn Any - } - fn as_box_any(self: Box) -> Box { - self as Box - } - fn type_name(&self) -> &'static str { - type_name::() - } - fn into_dynamic(self) -> Dynamic { - Dynamic::from(self) - } - fn clone_into_dynamic(&self) -> Dynamic { - Dynamic::from(self.clone()) - } - fn _closed(&self) -> _Private { - _Private - } -} - /// Trait to represent any type. /// /// `From<_>` is implemented for `i64` (`i32` if `only_i32`), `f64` (if not `no_float`), @@ -110,8 +89,7 @@ pub trait Variant: Any + Send + Sync { fn _closed(&self) -> _Private; } -#[cfg(feature = "sync")] -impl Variant for T { +impl Variant for T { fn as_any(&self) -> &dyn Any { self as &dyn Any } @@ -160,7 +138,6 @@ pub enum Union { Array(Box), #[cfg(not(feature = "no_object"))] Map(Box), - #[cfg(not(feature = "no_module"))] Module(Box), Variant(Box>), } @@ -198,7 +175,6 @@ impl Dynamic { Union::Array(_) => TypeId::of::(), #[cfg(not(feature = "no_object"))] Union::Map(_) => TypeId::of::(), - #[cfg(not(feature = "no_module"))] Union::Module(_) => TypeId::of::(), Union::Variant(value) => (***value).type_id(), } @@ -218,10 +194,10 @@ impl Dynamic { Union::Array(_) => "array", #[cfg(not(feature = "no_object"))] Union::Map(_) => "map", - #[cfg(not(feature = "no_module"))] Union::Module(_) => "sub-scope", #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] Union::Variant(value) if value.is::() => "timestamp", Union::Variant(value) => (***value).type_name(), } @@ -232,20 +208,20 @@ impl fmt::Display for Dynamic { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { Union::Unit(_) => write!(f, ""), - Union::Bool(value) => write!(f, "{}", value), - Union::Str(value) => write!(f, "{}", value), - Union::Char(value) => write!(f, "{}", value), - Union::Int(value) => write!(f, "{}", value), + Union::Bool(value) => fmt::Display::fmt(value, f), + Union::Str(value) => fmt::Display::fmt(value, f), + Union::Char(value) => fmt::Display::fmt(value, f), + Union::Int(value) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_float"))] - Union::Float(value) => write!(f, "{}", value), + Union::Float(value) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_index"))] - Union::Array(value) => write!(f, "{:?}", value), + Union::Array(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_object"))] Union::Map(value) => write!(f, "#{:?}", value), - #[cfg(not(feature = "no_module"))] - Union::Module(value) => write!(f, "{:?}", value), + Union::Module(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] Union::Variant(value) if value.is::() => write!(f, ""), Union::Variant(_) => write!(f, "?"), } @@ -255,21 +231,21 @@ impl fmt::Display for Dynamic { impl fmt::Debug for Dynamic { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { - Union::Unit(value) => write!(f, "{:?}", value), - Union::Bool(value) => write!(f, "{:?}", value), - Union::Str(value) => write!(f, "{:?}", value), - Union::Char(value) => write!(f, "{:?}", value), - Union::Int(value) => write!(f, "{:?}", value), + Union::Unit(value) => fmt::Debug::fmt(value, f), + Union::Bool(value) => fmt::Debug::fmt(value, f), + Union::Str(value) => fmt::Debug::fmt(value, f), + Union::Char(value) => fmt::Debug::fmt(value, f), + Union::Int(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_float"))] - Union::Float(value) => write!(f, "{:?}", value), + Union::Float(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_index"))] - Union::Array(value) => write!(f, "{:?}", value), + Union::Array(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_object"))] Union::Map(value) => write!(f, "#{:?}", value), - #[cfg(not(feature = "no_module"))] - Union::Module(value) => write!(f, "{:?}", value), + Union::Module(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] Union::Variant(value) if value.is::() => write!(f, ""), Union::Variant(_) => write!(f, ""), } @@ -290,7 +266,6 @@ impl Clone for Dynamic { Union::Array(ref value) => Self(Union::Array(value.clone())), #[cfg(not(feature = "no_object"))] Union::Map(ref value) => Self(Union::Map(value.clone())), - #[cfg(not(feature = "no_module"))] Union::Module(ref value) => Self(Union::Module(value.clone())), Union::Variant(ref value) => (***value).clone_into_dynamic(), } @@ -426,7 +401,6 @@ impl Dynamic { Union::Array(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), #[cfg(not(feature = "no_object"))] Union::Map(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), - #[cfg(not(feature = "no_module"))] Union::Module(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), Union::Variant(value) => (*value).as_box_any().downcast().map(|x| *x).ok(), } @@ -470,7 +444,6 @@ impl Dynamic { Union::Array(value) => *unsafe_cast_box::<_, T>(value).unwrap(), #[cfg(not(feature = "no_object"))] Union::Map(value) => *unsafe_cast_box::<_, T>(value).unwrap(), - #[cfg(not(feature = "no_module"))] Union::Module(value) => *unsafe_cast_box::<_, T>(value).unwrap(), Union::Variant(value) => (*value).as_box_any().downcast().map(|x| *x).unwrap(), } @@ -498,7 +471,6 @@ impl Dynamic { Union::Array(value) => (value.as_ref() as &dyn Any).downcast_ref::(), #[cfg(not(feature = "no_object"))] Union::Map(value) => (value.as_ref() as &dyn Any).downcast_ref::(), - #[cfg(not(feature = "no_module"))] Union::Module(value) => (value.as_ref() as &dyn Any).downcast_ref::(), Union::Variant(value) => value.as_ref().as_ref().as_any().downcast_ref::(), } @@ -524,7 +496,6 @@ impl Dynamic { Union::Array(value) => (value.as_mut() as &mut dyn Any).downcast_mut::(), #[cfg(not(feature = "no_object"))] Union::Map(value) => (value.as_mut() as &mut dyn Any).downcast_mut::(), - #[cfg(not(feature = "no_module"))] Union::Module(value) => (value.as_mut() as &mut dyn Any).downcast_mut::(), Union::Variant(value) => value.as_mut().as_mut_any().downcast_mut::(), } diff --git a/src/api.rs b/src/api.rs index 1d593023..ebfb699c 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,13 +1,16 @@ //! Module that defines the extern API of `Engine`. use crate::any::{Dynamic, Variant}; -use crate::engine::{make_getter, make_setter, Engine, State, FUNC_INDEXER}; +use crate::engine::{ + get_script_function_by_signature, make_getter, make_setter, Engine, State, FUNC_INDEXER_GET, + FUNC_INDEXER_SET, +}; use crate::error::ParseError; use crate::fn_call::FuncArgs; use crate::fn_native::{IteratorFn, SendSync}; use crate::fn_register::RegisterFn; use crate::optimize::{optimize_into_ast, OptimizationLevel}; -use crate::parser::{parse, parse_global_expr, AST}; +use crate::parser::AST; use crate::result::EvalAltResult; use crate::scope::Scope; use crate::token::{lex, Position}; @@ -24,6 +27,7 @@ use crate::stdlib::{ }; #[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] use crate::stdlib::{fs::File, io::prelude::*, path::PathBuf}; /// Engine public API @@ -273,7 +277,7 @@ impl Engine { self.register_set(name, set_fn); } - /// Register an indexer function for a registered type with the `Engine`. + /// Register an index getter for a registered type with the `Engine`. /// /// The function signature must start with `&mut self` and not `&self`. /// @@ -303,7 +307,7 @@ impl Engine { /// engine.register_fn("new_ts", TestStruct::new); /// /// // Register an indexer. - /// engine.register_indexer(TestStruct::get_field); + /// engine.register_indexer_get(TestStruct::get_field); /// /// assert_eq!(engine.eval::("let a = new_ts(); a[2]")?, 3); /// # Ok(()) @@ -311,7 +315,7 @@ impl Engine { /// ``` #[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_index"))] - pub fn register_indexer( + pub fn register_indexer_get( &mut self, callback: impl Fn(&mut T, X) -> U + SendSync + 'static, ) where @@ -319,7 +323,103 @@ impl Engine { U: Variant + Clone, X: Variant + Clone, { - self.register_fn(FUNC_INDEXER, callback); + self.register_fn(FUNC_INDEXER_GET, callback); + } + + /// Register an index setter for a registered type with the `Engine`. + /// + /// # Example + /// + /// ``` + /// #[derive(Clone)] + /// struct TestStruct { + /// fields: Vec + /// } + /// + /// impl TestStruct { + /// fn new() -> Self { TestStruct { fields: vec![1, 2, 3, 4, 5] } } + /// fn set_field(&mut self, index: i64, value: i64) { self.fields[index as usize] = value; } + /// } + /// + /// # fn main() -> Result<(), Box> { + /// use rhai::{Engine, RegisterFn}; + /// + /// let mut engine = Engine::new(); + /// + /// // Register the custom type. + /// engine.register_type::(); + /// + /// engine.register_fn("new_ts", TestStruct::new); + /// + /// // Register an indexer. + /// engine.register_indexer_set(TestStruct::set_field); + /// + /// assert_eq!( + /// engine.eval::("let a = new_ts(); a[2] = 42; a")?.fields[2], + /// 42 + /// ); + /// # Ok(()) + /// # } + /// ``` + #[cfg(not(feature = "no_object"))] + #[cfg(not(feature = "no_index"))] + pub fn register_indexer_set( + &mut self, + callback: impl Fn(&mut T, X, U) -> () + SendSync + 'static, + ) where + T: Variant + Clone, + U: Variant + Clone, + X: Variant + Clone, + { + self.register_fn(FUNC_INDEXER_SET, callback); + } + + /// Shorthand for register both index getter and setter functions for a registered type with the `Engine`. + /// + /// # Example + /// + /// ``` + /// #[derive(Clone)] + /// struct TestStruct { + /// fields: Vec + /// } + /// + /// impl TestStruct { + /// fn new() -> Self { TestStruct { fields: vec![1, 2, 3, 4, 5] } } + /// fn get_field(&mut self, index: i64) -> i64 { self.fields[index as usize] } + /// fn set_field(&mut self, index: i64, value: i64) { self.fields[index as usize] = value; } + /// } + /// + /// # fn main() -> Result<(), Box> { + /// use rhai::{Engine, RegisterFn}; + /// + /// let mut engine = Engine::new(); + /// + /// // Register the custom type. + /// engine.register_type::(); + /// + /// engine.register_fn("new_ts", TestStruct::new); + /// + /// // Register an indexer. + /// engine.register_indexer_get_set(TestStruct::get_field, TestStruct::set_field); + /// + /// assert_eq!(engine.eval::("let a = new_ts(); a[2] = 42; a[2]")?, 42); + /// # Ok(()) + /// # } + /// ``` + #[cfg(not(feature = "no_object"))] + #[cfg(not(feature = "no_index"))] + pub fn register_indexer_get_set( + &mut self, + getter: impl Fn(&mut T, X) -> U + SendSync + 'static, + setter: impl Fn(&mut T, X, U) -> () + SendSync + 'static, + ) where + T: Variant + Clone, + U: Variant + Clone, + X: Variant + Clone, + { + self.register_indexer_get(getter); + self.register_indexer_set(setter); } /// Compile a string into an `AST`, which can be used later for evaluation. @@ -448,19 +548,13 @@ impl Engine { scripts: &[&str], optimization_level: OptimizationLevel, ) -> Result { - let stream = lex(scripts); - - parse( - &mut stream.peekable(), - self, - scope, - optimization_level, - (self.max_expr_depth, self.max_function_expr_depth), - ) + let stream = lex(scripts, self.max_string_size); + self.parse(&mut stream.peekable(), scope, optimization_level) } /// Read the contents of a file into a string. #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] fn read_file(path: PathBuf) -> Result> { let mut f = File::open(path.clone()).map_err(|err| { Box::new(EvalAltResult::ErrorReadingScriptFile( @@ -504,6 +598,7 @@ impl Engine { /// # } /// ``` #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] pub fn compile_file(&self, path: PathBuf) -> Result> { self.compile_file_with_scope(&Scope::new(), path) } @@ -540,6 +635,7 @@ impl Engine { /// # } /// ``` #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] pub fn compile_file_with_scope( &self, scope: &Scope, @@ -577,14 +673,9 @@ impl Engine { // Trims the JSON string and add a '#' in front let scripts = ["#", json.trim()]; - let stream = lex(&scripts); - let ast = parse_global_expr( - &mut stream.peekable(), - self, - &scope, - OptimizationLevel::None, - self.max_expr_depth, - )?; + let stream = lex(&scripts, self.max_string_size); + let ast = + self.parse_global_expr(&mut stream.peekable(), &scope, OptimizationLevel::None)?; // Handle null - map to () if has_null { @@ -663,17 +754,10 @@ impl Engine { script: &str, ) -> Result { let scripts = [script]; - let stream = lex(&scripts); - + let stream = lex(&scripts, self.max_string_size); { let mut peekable = stream.peekable(); - parse_global_expr( - &mut peekable, - self, - scope, - self.optimization_level, - self.max_expr_depth, - ) + self.parse_global_expr(&mut peekable, scope, self.optimization_level) } } @@ -693,6 +777,7 @@ impl Engine { /// # } /// ``` #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] pub fn eval_file(&self, path: PathBuf) -> Result> { Self::read_file(path).and_then(|contents| self.eval::(&contents)) } @@ -717,6 +802,7 @@ impl Engine { /// # } /// ``` #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] pub fn eval_file_with_scope( &self, scope: &mut Scope, @@ -823,15 +909,10 @@ impl Engine { script: &str, ) -> Result> { let scripts = [script]; - let stream = lex(&scripts); + let stream = lex(&scripts, self.max_string_size); - let ast = parse_global_expr( - &mut stream.peekable(), - self, - scope, - OptimizationLevel::None, // No need to optimize a lone expression - self.max_expr_depth, - )?; + // No need to optimize a lone expression + let ast = self.parse_global_expr(&mut stream.peekable(), scope, OptimizationLevel::None)?; self.eval_ast_with_scope(scope, &ast) } @@ -904,6 +985,7 @@ impl Engine { }); } + /// Evaluate an `AST` with own scope. pub(crate) fn eval_ast_with_scope_raw( &self, scope: &mut Scope, @@ -926,6 +1008,7 @@ impl Engine { /// Evaluate a file, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] pub fn consume_file(&self, path: PathBuf) -> Result<(), Box> { Self::read_file(path).and_then(|contents| self.consume(&contents)) } @@ -933,6 +1016,7 @@ impl Engine { /// Evaluate a file with own scope, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] pub fn consume_file_with_scope( &self, scope: &mut Scope, @@ -955,15 +1039,8 @@ impl Engine { script: &str, ) -> Result<(), Box> { let scripts = [script]; - let stream = lex(&scripts); - - let ast = parse( - &mut stream.peekable(), - self, - scope, - self.optimization_level, - (self.max_expr_depth, self.max_function_expr_depth), - )?; + let stream = lex(&scripts, self.max_string_size); + let ast = self.parse(&mut stream.peekable(), scope, self.optimization_level)?; self.consume_ast_with_scope(scope, &ast) } @@ -1041,7 +1118,7 @@ impl Engine { args: A, ) -> Result> { let mut arg_values = args.into_vec(); - let result = self.call_fn_dynamic(scope, ast, name, arg_values.as_mut())?; + let result = self.call_fn_dynamic_raw(scope, ast, name, arg_values.as_mut())?; let return_type = self.map_type_name(result.type_name()); @@ -1055,13 +1132,6 @@ impl Engine { /// Call a script function defined in an `AST` with multiple `Dynamic` arguments. /// - /// ## WARNING - /// - /// All the arguments are _consumed_, meaning that they're replaced by `()`. - /// This is to avoid unnecessarily cloning the arguments. - /// Do you use the arguments after this call. If you need them afterwards, - /// clone them _before_ calling this function. - /// /// # Example /// /// ``` @@ -1082,13 +1152,13 @@ impl Engine { /// scope.push("foo", 42_i64); /// /// // Call the script-defined function - /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add", &mut [ String::from("abc").into(), 123_i64.into() ])?; + /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add", vec![ String::from("abc").into(), 123_i64.into() ])?; /// assert_eq!(result.cast::(), 168); /// - /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add1", &mut [ String::from("abc").into() ])?; + /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add1", vec![ String::from("abc").into() ])?; /// assert_eq!(result.cast::(), 46); /// - /// let result= engine.call_fn_dynamic(&mut scope, &ast, "bar", &mut [])?; + /// let result= engine.call_fn_dynamic(&mut scope, &ast, "bar", vec![])?; /// assert_eq!(result.cast::(), 21); /// # } /// # Ok(()) @@ -1096,6 +1166,25 @@ impl Engine { /// ``` #[cfg(not(feature = "no_function"))] pub fn call_fn_dynamic( + &self, + scope: &mut Scope, + ast: &AST, + name: &str, + arg_values: impl IntoIterator, + ) -> Result> { + let mut arg_values: StaticVec<_> = arg_values.into_iter().collect(); + self.call_fn_dynamic_raw(scope, ast, name, arg_values.as_mut()) + } + + /// Call a script function defined in an `AST` with multiple `Dynamic` arguments. + /// + /// ## WARNING + /// + /// All the arguments are _consumed_, meaning that they're replaced by `()`. + /// This is to avoid unnecessarily cloning the arguments. + /// Do not use the arguments after this call. If they are needed afterwards, + /// clone them _before_ calling this function. + pub(crate) fn call_fn_dynamic_raw( &self, scope: &mut Scope, ast: &AST, @@ -1103,17 +1192,18 @@ impl Engine { arg_values: &mut [Dynamic], ) -> Result> { let mut args: StaticVec<_> = arg_values.iter_mut().collect(); - let lib = ast.lib(); - let pos = Position::none(); - - let fn_def = lib - .get_function_by_signature(name, args.len(), true) - .ok_or_else(|| Box::new(EvalAltResult::ErrorFunctionNotFound(name.into(), pos)))?; + let fn_def = get_script_function_by_signature(ast.lib(), name, args.len(), true) + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorFunctionNotFound( + name.into(), + Position::none(), + )) + })?; let mut state = State::new(); let args = args.as_mut(); - self.call_script_fn(scope, &mut state, &lib, name, fn_def, args, pos, 0) + self.call_script_fn(scope, &mut state, ast.lib(), name, fn_def, args, 0) } /// Optimize the `AST` with constants defined in an external Scope. @@ -1136,8 +1226,9 @@ impl Engine { ) -> AST { let lib = ast .lib() - .iter() - .map(|(_, fn_def)| fn_def.as_ref().clone()) + .iter_fn() + .filter(|(_, _, _, f)| f.is_script()) + .map(|(_, _, _, f)| f.get_fn_def().clone()) .collect(); let stmt = mem::take(ast.statements_mut()); @@ -1159,7 +1250,7 @@ impl Engine { /// /// let mut engine = Engine::new(); /// - /// engine.on_progress(move |ops| { + /// engine.on_progress(move |&ops| { /// if ops > 10000 { /// false /// } else if ops % 800 == 0 { @@ -1178,7 +1269,7 @@ impl Engine { /// # Ok(()) /// # } /// ``` - pub fn on_progress(&mut self, callback: impl Fn(u64) -> bool + SendSync + 'static) { + pub fn on_progress(&mut self, callback: impl Fn(&u64) -> bool + SendSync + 'static) { self.progress = Some(Box::new(callback)); } diff --git a/src/engine.rs b/src/engine.rs index 03521f4f..625fb338 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,25 +1,22 @@ //! Main module defining the script evaluation `Engine`. -use crate::any::{Dynamic, Union}; +use crate::any::{Dynamic, Union, Variant}; use crate::calc_fn_hash; use crate::error::ParseErrorType; -use crate::fn_native::{CallableFunction, FnCallArgs, Shared}; -use crate::module::Module; +use crate::fn_native::{CallableFunction, Callback, FnCallArgs}; +use crate::module::{resolvers, Module, ModuleResolver}; use crate::optimize::OptimizationLevel; -use crate::packages::{CorePackage, Package, PackageLibrary, PackagesCollection, StandardPackage}; -use crate::parser::{Expr, FnAccess, FnDef, ImmutableString, ReturnType, Stmt, AST, INT}; +use crate::packages::{Package, PackageLibrary, PackagesCollection, StandardPackage}; +use crate::parser::{Expr, FnAccess, ImmutableString, ReturnType, ScriptFnDef, Stmt, AST, INT}; use crate::r#unsafe::{unsafe_cast_var_name_to_lifetime, unsafe_mut_cast_to_lifetime}; use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; use crate::token::Position; -use crate::utils::{StaticVec, StraightHasherBuilder}; +use crate::utils::StaticVec; #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; -#[cfg(not(feature = "no_module"))] -use crate::module::{resolvers, ModuleResolver}; - use crate::stdlib::{ any::TypeId, boxed::Box, @@ -27,7 +24,6 @@ use crate::stdlib::{ format, iter::{empty, once}, mem, - ops::{Deref, DerefMut}, string::{String, ToString}, vec::Vec, }; @@ -67,9 +63,9 @@ pub const MAX_FUNCTION_EXPR_DEPTH: usize = 32; #[cfg(feature = "unchecked")] pub const MAX_CALL_STACK_DEPTH: usize = usize::MAX; #[cfg(feature = "unchecked")] -pub const MAX_EXPR_DEPTH: usize = usize::MAX; +pub const MAX_EXPR_DEPTH: usize = 0; #[cfg(feature = "unchecked")] -pub const MAX_FUNCTION_EXPR_DEPTH: usize = usize::MAX; +pub const MAX_FUNCTION_EXPR_DEPTH: usize = 0; pub const KEYWORD_PRINT: &str = "print"; pub const KEYWORD_DEBUG: &str = "debug"; @@ -78,9 +74,11 @@ pub const KEYWORD_EVAL: &str = "eval"; pub const FUNC_TO_STRING: &str = "to_string"; pub const FUNC_GETTER: &str = "get$"; pub const FUNC_SETTER: &str = "set$"; -pub const FUNC_INDEXER: &str = "$index$"; +pub const FUNC_INDEXER_GET: &str = "$index$get$"; +pub const FUNC_INDEXER_SET: &str = "$index$set$"; /// A type that encapsulates a mutation target for an expression with side effects. +#[derive(Debug)] enum Target<'a> { /// The target is a mutable reference to a `Dynamic` value somewhere. Ref(&'a mut Dynamic), @@ -95,41 +93,57 @@ impl Target<'_> { /// Is the `Target` a reference pointing to other data? pub fn is_ref(&self) -> bool { match self { - Target::Ref(_) => true, - Target::Value(_) | Target::StringChar(_, _, _) => false, + Self::Ref(_) => true, + Self::Value(_) | Self::StringChar(_, _, _) => false, + } + } + /// Is the `Target` an owned value? + pub fn is_value(&self) -> bool { + match self { + Self::Ref(_) => false, + Self::Value(_) => true, + Self::StringChar(_, _, _) => false, + } + } + /// Is the `Target` a specific type? + pub fn is(&self) -> bool { + match self { + Target::Ref(r) => r.is::(), + Target::Value(r) => r.is::(), + Target::StringChar(_, _, _) => TypeId::of::() == TypeId::of::(), } } - /// Get the value of the `Target` as a `Dynamic`, cloning a referenced value if necessary. pub fn clone_into_dynamic(self) -> Dynamic { match self { - Target::Ref(r) => r.clone(), // Referenced value is cloned - Target::Value(v) => v, // Owned value is simply taken - Target::StringChar(_, _, ch) => ch, // Character is taken + Self::Ref(r) => r.clone(), // Referenced value is cloned + Self::Value(v) => v, // Owned value is simply taken + Self::StringChar(_, _, ch) => ch, // Character is taken } } - /// Get a mutable reference from the `Target`. pub fn as_mut(&mut self) -> &mut Dynamic { match self { - Target::Ref(r) => *r, - Target::Value(ref mut r) => r, - Target::StringChar(_, _, ref mut r) => r, + Self::Ref(r) => *r, + Self::Value(ref mut r) => r, + Self::StringChar(_, _, ref mut r) => r, } } - /// Update the value of the `Target`. - pub fn set_value(&mut self, new_val: Dynamic, pos: Position) -> Result<(), Box> { + /// Position in `EvalAltResult` is None and must be set afterwards. + pub fn set_value(&mut self, new_val: Dynamic) -> Result<(), Box> { match self { - Target::Ref(r) => **r = new_val, - Target::Value(_) => { - return Err(Box::new(EvalAltResult::ErrorAssignmentToUnknownLHS(pos))) + Self::Ref(r) => **r = new_val, + Self::Value(_) => { + return Err(Box::new(EvalAltResult::ErrorAssignmentToUnknownLHS( + Position::none(), + ))) } - Target::StringChar(Dynamic(Union::Str(ref mut s)), index, _) => { + Self::StringChar(Dynamic(Union::Str(ref mut s)), index, _) => { // Replace the character at the specified index position let new_ch = new_val .as_char() - .map_err(|_| EvalAltResult::ErrorCharMismatch(pos))?; + .map_err(|_| EvalAltResult::ErrorCharMismatch(Position::none()))?; let mut chars: StaticVec = s.chars().collect(); let ch = chars[*index]; @@ -164,22 +178,19 @@ impl> From for Target<'_> { /// /// This type uses some unsafe code, mainly for avoiding cloning of local variable names via /// direct lifetime casting. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Eq, PartialEq, Hash, Clone, Default)] pub struct State { /// Normally, access to variables are parsed with a relative offset into the scope to avoid a lookup. /// In some situation, e.g. after running an `eval` statement, subsequent offsets become mis-aligned. /// When that happens, this flag is turned on to force a scope lookup by name. pub always_search: bool, - /// Level of the current scope. The global (root) level is zero, a new block (or function call) /// is one level higher, and so on. pub scope_level: usize, - /// Number of operations performed. pub operations: u64, - /// Number of modules loaded. - pub modules: u64, + pub modules: usize, } impl State { @@ -189,88 +200,24 @@ impl State { } } -/// A type that holds a library (`HashMap`) of script-defined functions. -/// -/// Since script-defined functions have `Dynamic` parameters, functions with the same name -/// and number of parameters are considered equivalent. -/// -/// The key of the `HashMap` is a `u64` hash calculated by the function `calc_fn_hash`. -#[derive(Debug, Clone, Default)] -pub struct FunctionsLib(HashMap, StraightHasherBuilder>); +/// Get a script-defined function definition from a module. +pub fn get_script_function_by_signature<'a>( + module: &'a Module, + name: &str, + params: usize, + public_only: bool, +) -> Option<&'a ScriptFnDef> { + // Qualifiers (none) + function name + number of arguments. + let hash_script = calc_fn_hash(empty(), name, params, empty()); + let func = module.get_fn(hash_script)?; + if !func.is_script() { + return None; + } + let fn_def = func.get_fn_def(); -impl FunctionsLib { - /// Create a new `FunctionsLib` from a collection of `FnDef`. - pub fn from_iter(vec: impl IntoIterator) -> Self { - FunctionsLib( - vec.into_iter() - .map(|fn_def| { - // Qualifiers (none) + function name + number of arguments. - let hash = calc_fn_hash(empty(), &fn_def.name, fn_def.params.len(), empty()); - (hash, fn_def.into()) - }) - .collect(), - ) - } - /// Does a certain function exist in the `FunctionsLib`? - /// - /// The `u64` hash is calculated by the function `crate::calc_fn_hash`. - pub fn has_function(&self, hash_fn_def: u64) -> bool { - self.contains_key(&hash_fn_def) - } - /// Get a function definition from the `FunctionsLib`. - /// - /// The `u64` hash is calculated by the function `crate::calc_fn_hash`. - pub fn get_function(&self, hash_fn_def: u64) -> Option<&FnDef> { - self.get(&hash_fn_def).map(|fn_def| fn_def.as_ref()) - } - /// Get a function definition from the `FunctionsLib`. - pub fn get_function_by_signature( - &self, - name: &str, - params: usize, - public_only: bool, - ) -> Option<&FnDef> { - // Qualifiers (none) + function name + number of arguments. - let hash_fn_def = calc_fn_hash(empty(), name, params, empty()); - let fn_def = self.get_function(hash_fn_def); - - match fn_def.as_ref().map(|f| f.access) { - None => None, - Some(FnAccess::Private) if public_only => None, - Some(FnAccess::Private) | Some(FnAccess::Public) => fn_def, - } - } - /// Merge another `FunctionsLib` into this `FunctionsLib`. - pub fn merge(&self, other: &Self) -> Self { - if self.is_empty() { - other.clone() - } else if other.is_empty() { - self.clone() - } else { - let mut functions = self.clone(); - functions.extend(other.iter().map(|(hash, fn_def)| (*hash, fn_def.clone()))); - functions - } - } -} - -impl From)>> for FunctionsLib { - fn from(values: Vec<(u64, Shared)>) -> Self { - FunctionsLib(values.into_iter().collect()) - } -} - -impl Deref for FunctionsLib { - type Target = HashMap, StraightHasherBuilder>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for FunctionsLib { - fn deref_mut(&mut self) -> &mut HashMap, StraightHasherBuilder> { - &mut self.0 + match fn_def.access { + FnAccess::Private if public_only => None, + FnAccess::Private | FnAccess::Public => Some(&fn_def), } } @@ -289,40 +236,28 @@ impl DerefMut for FunctionsLib { /// # } /// ``` /// -/// Currently, `Engine` is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`. +/// Currently, `Engine` is neither `Send` nor `Sync`. Use the `sync` feature to make it `Send + Sync`. pub struct Engine { + /// A unique ID identifying this scripting `Engine`. + pub id: Option, + /// A module containing all functions directly loaded into the Engine. pub(crate) global_module: Module, /// A collection of all library packages loaded into the Engine. pub(crate) packages: PackagesCollection, /// A module resolution service. - #[cfg(not(feature = "no_module"))] pub(crate) module_resolver: Option>, /// A hashmap mapping type names to pretty-print names. pub(crate) type_names: HashMap, - /// Closure for implementing the `print` command. - #[cfg(not(feature = "sync"))] - pub(crate) print: Box, - /// Closure for implementing the `print` command. - #[cfg(feature = "sync")] - pub(crate) print: Box, - - /// Closure for implementing the `debug` command. - #[cfg(not(feature = "sync"))] - pub(crate) debug: Box, - /// Closure for implementing the `debug` command. - #[cfg(feature = "sync")] - pub(crate) debug: Box, - - /// Closure for progress reporting. - #[cfg(not(feature = "sync"))] - pub(crate) progress: Option bool + 'static>>, - /// Closure for progress reporting. - #[cfg(feature = "sync")] - pub(crate) progress: Option bool + Send + Sync + 'static>>, + /// Callback closure for implementing the `print` command. + pub(crate) print: Callback, + /// Callback closure for implementing the `debug` command. + pub(crate) debug: Callback, + /// Callback closure for progress reporting. + pub(crate) progress: Option>, /// Optimize the AST after compilation. pub(crate) optimization_level: OptimizationLevel, @@ -337,21 +272,29 @@ pub struct Engine { /// Maximum number of operations allowed to run. pub(crate) max_operations: u64, /// Maximum number of modules allowed to load. - pub(crate) max_modules: u64, + pub(crate) max_modules: usize, + /// Maximum length of a string. + pub(crate) max_string_size: usize, + /// Maximum length of an array. + pub(crate) max_array_size: usize, + /// Maximum number of properties in a map. + pub(crate) max_map_size: usize, } impl Default for Engine { fn default() -> Self { // Create the new scripting Engine let mut engine = Self { + id: None, + packages: Default::default(), global_module: Default::default(), #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] module_resolver: Some(Box::new(resolvers::FileModuleResolver::new())), - #[cfg(not(feature = "no_module"))] - #[cfg(feature = "no_std")] + #[cfg(any(feature = "no_module", feature = "no_std", target_arch = "wasm32",))] module_resolver: None, type_names: HashMap::new(), @@ -373,8 +316,11 @@ impl Default for Engine { max_call_stack_depth: MAX_CALL_STACK_DEPTH, max_expr_depth: MAX_EXPR_DEPTH, max_function_expr_depth: MAX_FUNCTION_EXPR_DEPTH, - max_operations: u64::MAX, - max_modules: u64::MAX, + max_operations: 0, + max_modules: usize::MAX, + max_string_size: 0, + max_array_size: 0, + max_map_size: 0, }; engine.load_package(StandardPackage::new().get()); @@ -428,6 +374,7 @@ fn extract_prop_from_setter(fn_name: &str) -> Option<&str> { /// Print/debug to stdout fn default_print(s: &str) { #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] println!("{}", s); } @@ -442,46 +389,51 @@ fn search_scope<'s, 'a>( _ => unreachable!(), }; - #[cfg(not(feature = "no_module"))] - { - if let Some(modules) = modules.as_ref() { - let module = if let Some(index) = modules.index() { - scope - .get_mut(scope.len() - index.get()) - .0 - .downcast_mut::() - .unwrap() - } else { - let (id, root_pos) = modules.get(0); + // Check if it is qualified + if let Some(modules) = modules.as_ref() { + // Qualified - check if the root module is directly indexed + let index = if state.always_search { + None + } else { + modules.index() + }; - scope.find_module(id).ok_or_else(|| { - Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)) - })? - }; + let module = if let Some(index) = index { + scope + .get_mut(scope.len() - index.get()) + .0 + .downcast_mut::() + .unwrap() + } else { + // Find the root module in the scope + let (id, root_pos) = modules.get(0); - return Ok(( - module.get_qualified_var_mut(name, *hash_var, *pos)?, - name, - // Module variables are constant - ScopeEntryType::Constant, - *pos, - )); - } - } + scope + .find_module_internal(id) + .ok_or_else(|| Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)))? + }; - let index = if state.always_search { None } else { *index }; + let target = module.get_qualified_var_mut(name, *hash_var, *pos)?; - let index = if let Some(index) = index { - scope.len() - index.get() + // Module variables are constant + Ok((target, name, ScopeEntryType::Constant, *pos)) } else { - scope - .get_index(name) - .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.into(), *pos)))? - .0 - }; + // Unqualified - check if it is directly indexed + let index = if state.always_search { None } else { *index }; - let (val, typ) = scope.get_mut(index); - Ok((val, name, typ, *pos)) + let index = if let Some(index) = index { + scope.len() - index.get() + } else { + // Find the variable in the scope + scope + .get_index(name) + .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.into(), *pos)))? + .0 + }; + + let (val, typ) = scope.get_mut(index); + Ok((val, name, typ, *pos)) + } } impl Engine { @@ -494,10 +446,10 @@ impl Engine { /// Use the `load_package` method to load additional packages of functions. pub fn new_raw() -> Self { Self { + id: None, + packages: Default::default(), global_module: Default::default(), - - #[cfg(not(feature = "no_module"))] module_resolver: None, type_names: HashMap::new(), @@ -514,8 +466,11 @@ impl Engine { max_call_stack_depth: MAX_CALL_STACK_DEPTH, max_expr_depth: MAX_EXPR_DEPTH, max_function_expr_depth: MAX_FUNCTION_EXPR_DEPTH, - max_operations: u64::MAX, - max_modules: u64::MAX, + max_operations: 0, + max_modules: usize::MAX, + max_string_size: 0, + max_array_size: 0, + max_map_size: 0, } } @@ -545,6 +500,15 @@ impl Engine { self.optimization_level = optimization_level } + /// The current optimization level. + /// It controls whether and how the `Engine` will optimize an AST after compilation. + /// + /// Not available under the `no_optimize` feature. + #[cfg(not(feature = "no_optimize"))] + pub fn optimization_level(&self) -> OptimizationLevel { + self.optimization_level + } + /// Set the maximum levels of function calls allowed for a script in order to avoid /// infinite recursion and stack overflows. #[cfg(not(feature = "unchecked"))] @@ -552,28 +516,106 @@ impl Engine { self.max_call_stack_depth = levels } + /// The maximum levels of function calls allowed for a script. + #[cfg(not(feature = "unchecked"))] + pub fn max_call_levels(&self) -> usize { + self.max_call_stack_depth + } + /// Set the maximum number of operations allowed for a script to run to avoid /// consuming too much resources (0 for unlimited). #[cfg(not(feature = "unchecked"))] pub fn set_max_operations(&mut self, operations: u64) { - self.max_operations = if operations == 0 { - u64::MAX + self.max_operations = if operations == u64::MAX { + 0 } else { operations }; } - /// Set the maximum number of imported modules allowed for a script (0 for unlimited). + /// The maximum number of operations allowed for a script to run (0 for unlimited). #[cfg(not(feature = "unchecked"))] - pub fn set_max_modules(&mut self, modules: u64) { - self.max_modules = if modules == 0 { u64::MAX } else { modules }; + pub fn max_operations(&self) -> u64 { + self.max_operations } - /// Set the depth limits for expressions/statements. + /// Set the maximum number of imported modules allowed for a script. + #[cfg(not(feature = "unchecked"))] + pub fn set_max_modules(&mut self, modules: usize) { + self.max_modules = modules; + } + + /// The maximum number of imported modules allowed for a script. + #[cfg(not(feature = "unchecked"))] + pub fn max_modules(&self) -> usize { + self.max_modules + } + + /// Set the depth limits for expressions (0 for unlimited). #[cfg(not(feature = "unchecked"))] pub fn set_max_expr_depths(&mut self, max_expr_depth: usize, max_function_expr_depth: usize) { - self.max_expr_depth = max_expr_depth; - self.max_function_expr_depth = max_function_expr_depth; + self.max_expr_depth = if max_expr_depth == usize::MAX { + 0 + } else { + max_expr_depth + }; + self.max_function_expr_depth = if max_function_expr_depth == usize::MAX { + 0 + } else { + max_function_expr_depth + }; + } + + /// The depth limit for expressions (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn max_expr_depth(&self) -> usize { + self.max_expr_depth + } + + /// The depth limit for expressions in functions (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn max_function_expr_depth(&self) -> usize { + self.max_function_expr_depth + } + + /// Set the maximum length of strings (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn set_max_string_size(&mut self, max_size: usize) { + self.max_string_size = if max_size == usize::MAX { 0 } else { max_size }; + } + + /// The maximum length of strings (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn max_string_size(&self) -> usize { + self.max_string_size + } + + /// Set the maximum length of arrays (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + #[cfg(not(feature = "no_index"))] + pub fn set_max_array_size(&mut self, max_size: usize) { + self.max_array_size = if max_size == usize::MAX { 0 } else { max_size }; + } + + /// The maximum length of arrays (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + #[cfg(not(feature = "no_index"))] + pub fn max_array_size(&self) -> usize { + self.max_array_size + } + + /// Set the maximum length of object maps (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + #[cfg(not(feature = "no_object"))] + pub fn set_max_map_size(&mut self, max_size: usize) { + self.max_map_size = if max_size == usize::MAX { 0 } else { max_size }; + } + + /// The maximum length of object maps (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + #[cfg(not(feature = "no_object"))] + pub fn max_map_size(&self) -> usize { + self.max_map_size } /// Set the module resolution service used by the `Engine`. @@ -585,6 +627,7 @@ impl Engine { } /// Universal method for calling functions either registered with the `Engine` or written in Rhai. + /// Position in `EvalAltResult` is None and must be set afterwards. /// /// ## WARNING /// @@ -595,25 +638,26 @@ impl Engine { &self, scope: &mut Scope, state: &mut State, - lib: &FunctionsLib, + lib: &Module, fn_name: &str, - hashes: (u64, u64), + (hash_fn, hash_script): (u64, u64), args: &mut FnCallArgs, is_ref: bool, def_val: Option<&Dynamic>, - pos: Position, level: usize, ) -> Result<(Dynamic, bool), Box> { - self.inc_operations(state, pos)?; + self.inc_operations(state)?; - let native_only = hashes.1 == 0; + let native_only = hash_script == 0; // Check for stack overflow #[cfg(not(feature = "no_function"))] #[cfg(not(feature = "unchecked"))] { if level > self.max_call_stack_depth { - return Err(Box::new(EvalAltResult::ErrorStackOverflow(pos))); + return Err(Box::new( + EvalAltResult::ErrorStackOverflow(Position::none()), + )); } } @@ -653,68 +697,72 @@ impl Engine { } } + // Search for the function // First search in script-defined functions (can override built-in) - if !native_only { - if let Some(fn_def) = lib.get(&hashes.1) { - normalize_first_arg(is_ref, &mut this_copy, &mut old_this_ptr, args); - - // Run scripted function - let result = - self.call_script_fn(scope, state, lib, fn_name, fn_def, args, pos, level)?; - - // Restore the original reference - restore_first_arg(old_this_ptr, args); - - return Ok((result, false)); - } + // Then search registered native functions (can override packages) + // Then search packages + // NOTE: We skip script functions for global_module and packages, and native functions for lib + let func = if !native_only { + lib.get_fn(hash_script) //.or_else(|| lib.get_fn(hash_fn)) + } else { + None } + //.or_else(|| self.global_module.get_fn(hash_script)) + .or_else(|| self.global_module.get_fn(hash_fn)) + //.or_else(|| self.packages.get_fn(hash_script)) + .or_else(|| self.packages.get_fn(hash_fn)); - // Search built-in's and external functions - if let Some(func) = self - .global_module - .get_fn(hashes.0) - .or_else(|| self.packages.get_fn(hashes.0)) - { + if let Some(func) = func { // Calling pure function in method-call? normalize_first_arg( - func.is_pure() && is_ref, + (func.is_pure() || func.is_script()) && is_ref, &mut this_copy, &mut old_this_ptr, args, ); - // Run external function - let result = func.get_native_fn()(args); + if func.is_script() { + // Run scripted function + let fn_def = func.get_fn_def(); + let result = + self.call_script_fn(scope, state, lib, fn_name, fn_def, args, level)?; - // Restore the original reference - restore_first_arg(old_this_ptr, args); + // Restore the original reference + restore_first_arg(old_this_ptr, args); - let result = result.map_err(|err| err.new_position(pos))?; + return Ok((result, false)); + } else { + // Run external function + let result = func.get_native_fn()(self, args)?; - // See if the function match print/debug (which requires special processing) - return Ok(match fn_name { - KEYWORD_PRINT => ( - (self.print)(result.as_str().map_err(|type_name| { - Box::new(EvalAltResult::ErrorMismatchOutputType( - type_name.into(), - pos, - )) - })?) - .into(), - false, - ), - KEYWORD_DEBUG => ( - (self.debug)(result.as_str().map_err(|type_name| { - Box::new(EvalAltResult::ErrorMismatchOutputType( - type_name.into(), - pos, - )) - })?) - .into(), - false, - ), - _ => (result, func.is_method()), - }); + // Restore the original reference + restore_first_arg(old_this_ptr, args); + + // See if the function match print/debug (which requires special processing) + return Ok(match fn_name { + KEYWORD_PRINT => ( + (self.print)(result.as_str().map_err(|type_name| { + Box::new(EvalAltResult::ErrorMismatchOutputType( + type_name.into(), + Position::none(), + )) + })?) + .into(), + false, + ), + KEYWORD_DEBUG => ( + (self.debug)(result.as_str().map_err(|type_name| { + Box::new(EvalAltResult::ErrorMismatchOutputType( + type_name.into(), + Position::none(), + )) + })?) + .into(), + false, + ), + _ => (result, func.is_method()), + }); + } } // See if it is built in. @@ -734,7 +782,7 @@ impl Engine { if let Some(prop) = extract_prop_from_getter(fn_name) { return Err(Box::new(EvalAltResult::ErrorDotExpr( format!("- property '{}' unknown or write-only", prop), - pos, + Position::none(), ))); } @@ -742,31 +790,54 @@ impl Engine { if let Some(prop) = extract_prop_from_setter(fn_name) { return Err(Box::new(EvalAltResult::ErrorDotExpr( format!("- property '{}' unknown or read-only", prop), - pos, + Position::none(), ))); } - let types_list: Vec<_> = args - .iter() - .map(|name| self.map_type_name(name.type_name())) - .collect(); - - // Getter function not found? - if fn_name == FUNC_INDEXER { + // index getter function not found? + if fn_name == FUNC_INDEXER_GET && args.len() == 2 { return Err(Box::new(EvalAltResult::ErrorFunctionNotFound( - format!("[]({})", types_list.join(", ")), - pos, + format!( + "{} [{}]", + self.map_type_name(args[0].type_name()), + self.map_type_name(args[1].type_name()), + ), + Position::none(), + ))); + } + + // index setter function not found? + if fn_name == FUNC_INDEXER_SET { + return Err(Box::new(EvalAltResult::ErrorFunctionNotFound( + format!( + "{} [{}]=", + self.map_type_name(args[0].type_name()), + self.map_type_name(args[1].type_name()), + ), + Position::none(), ))); } // Raise error Err(Box::new(EvalAltResult::ErrorFunctionNotFound( - format!("{} ({})", fn_name, types_list.join(", ")), - pos, + format!( + "{} ({})", + fn_name, + args.iter() + .map(|name| if name.is::() { + "&str | ImmutableString" + } else { + self.map_type_name((*name).type_name()) + }) + .collect::>() + .join(", ") + ), + Position::none(), ))) } /// Call a script-defined function. + /// Position in `EvalAltResult` is None and must be set afterwards. /// /// ## WARNING /// @@ -777,11 +848,10 @@ impl Engine { &self, scope: &mut Scope, state: &mut State, - lib: &FunctionsLib, + lib: &Module, fn_name: &str, - fn_def: &FnDef, + fn_def: &ScriptFnDef, args: &mut FnCallArgs, - pos: Position, level: usize, ) -> Result> { let orig_scope_level = state.scope_level; @@ -808,13 +878,17 @@ impl Engine { .or_else(|err| match *err { // Convert return statement to return value EvalAltResult::Return(x, _) => Ok(x), - EvalAltResult::ErrorInFunctionCall(name, err, _) => Err(Box::new( - EvalAltResult::ErrorInFunctionCall(format!("{} > {}", fn_name, name), err, pos), - )), + EvalAltResult::ErrorInFunctionCall(name, err, _) => { + Err(Box::new(EvalAltResult::ErrorInFunctionCall( + format!("{} > {}", fn_name, name), + err, + Position::none(), + ))) + } _ => Err(Box::new(EvalAltResult::ErrorInFunctionCall( fn_name.to_string(), err, - pos, + Position::none(), ))), }); @@ -826,16 +900,22 @@ impl Engine { } // Has a system function an override? - fn has_override(&self, lib: &FunctionsLib, hashes: (u64, u64)) -> bool { - // First check registered functions - self.global_module.contains_fn(hashes.0) - // Then check packages - || self.packages.contains_fn(hashes.0) - // Then check script-defined functions - || lib.contains_key(&hashes.1) + fn has_override(&self, lib: &Module, (hash_fn, hash_script): (u64, u64)) -> bool { + // NOTE: We skip script functions for global_module and packages, and native functions for lib + + // First check script-defined functions + lib.contains_fn(hash_script) + //|| lib.contains_fn(hash_fn) + // Then check registered functions + //|| self.global_module.contains_fn(hash_script) + || self.global_module.contains_fn(hash_fn) + // Then check packages + //|| self.packages.contains_fn(hash_script) + || self.packages.contains_fn(hash_fn) } - // Perform an actual function call, taking care of special functions + /// Perform an actual function call, taking care of special functions + /// Position in `EvalAltResult` is None and must be set afterwards. /// /// ## WARNING /// @@ -845,24 +925,19 @@ impl Engine { fn exec_fn_call( &self, state: &mut State, - lib: &FunctionsLib, + lib: &Module, fn_name: &str, native_only: bool, - hash_fn_def: u64, + hash_script: u64, args: &mut FnCallArgs, is_ref: bool, def_val: Option<&Dynamic>, - pos: Position, level: usize, ) -> Result<(Dynamic, bool), Box> { // Qualifiers (none) + function name + number of arguments + argument `TypeId`'s. - let hash_fn = calc_fn_hash( - empty(), - fn_name, - args.len(), - args.iter().map(|a| a.type_id()), - ); - let hashes = (hash_fn, if native_only { 0 } else { hash_fn_def }); + let arg_types = args.iter().map(|a| a.type_id()); + let hash_fn = calc_fn_hash(empty(), fn_name, args.len(), arg_types); + let hashes = (hash_fn, if native_only { 0 } else { hash_script }); match fn_name { // type_of @@ -875,7 +950,7 @@ impl Engine { KEYWORD_EVAL if args.len() == 1 && !self.has_override(lib, hashes) => { Err(Box::new(EvalAltResult::ErrorRuntime( "'eval' should not be called in method style. Try eval(...);".into(), - pos, + Position::none(), ))) } @@ -883,24 +958,24 @@ impl Engine { _ => { let mut scope = Scope::new(); self.call_fn_raw( - &mut scope, state, lib, fn_name, hashes, args, is_ref, def_val, pos, level, + &mut scope, state, lib, fn_name, hashes, args, is_ref, def_val, level, ) } } } /// Evaluate a text string as a script - used primarily for 'eval'. + /// Position in `EvalAltResult` is None and must be set afterwards. fn eval_script_expr( &self, scope: &mut Scope, state: &mut State, - lib: &FunctionsLib, + lib: &Module, script: &Dynamic, - pos: Position, ) -> Result> { - let script = script - .as_str() - .map_err(|type_name| EvalAltResult::ErrorMismatchOutputType(type_name.into(), pos))?; + let script = script.as_str().map_err(|type_name| { + EvalAltResult::ErrorMismatchOutputType(type_name.into(), Position::none()) + })?; // Compile the script text // No optimizations because we only run it once @@ -911,48 +986,45 @@ impl Engine { )?; // If new functions are defined within the eval string, it is an error - if !ast.lib().is_empty() { - return Err(Box::new(EvalAltResult::ErrorParsing( - ParseErrorType::WrongFnDefinition.into_err(pos), - ))); + if ast.lib().num_fn() != 0 { + return Err(ParseErrorType::WrongFnDefinition.into()); } let statements = mem::take(ast.statements_mut()); let ast = AST::new(statements, lib.clone()); // Evaluate the AST - let (result, operations) = self - .eval_ast_with_scope_raw(scope, &ast) - .map_err(|err| err.new_position(pos))?; + let (result, operations) = self.eval_ast_with_scope_raw(scope, &ast)?; state.operations += operations; - self.inc_operations(state, pos)?; + self.inc_operations(state)?; return Ok(result); } /// Chain-evaluate a dot/index chain. + /// Position in `EvalAltResult` is None and must be set afterwards. fn eval_dot_index_chain_helper( &self, state: &mut State, - lib: &FunctionsLib, + lib: &Module, target: &mut Target, rhs: &Expr, idx_values: &mut StaticVec, is_index: bool, - op_pos: Position, level: usize, mut new_val: Option, ) -> Result<(Dynamic, bool), Box> { let is_ref = target.is_ref(); - - // Get a reference to the mutation target Dynamic - let obj = target.as_mut(); + let is_value = target.is_value(); // Pop the last index value let mut idx_val = idx_values.pop(); if is_index { + #[cfg(feature = "no_index")] + unreachable!(); + let pos = rhs.position(); match rhs { @@ -961,25 +1033,62 @@ impl Engine { let (idx, expr, pos) = x.as_ref(); let is_idx = matches!(rhs, Expr::Index(_)); let idx_pos = idx.position(); - let this_ptr = &mut self.get_indexed_mut( - state, lib, obj, is_ref, idx_val, idx_pos, op_pos, false, - )?; + let this_ptr = + &mut self.get_indexed_mut(state, lib, target, idx_val, idx_pos, false)?; self.eval_dot_index_chain_helper( - state, lib, this_ptr, expr, idx_values, is_idx, *pos, level, new_val, + state, lib, this_ptr, expr, idx_values, is_idx, level, new_val, ) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } // xxx[rhs] = new_val _ if new_val.is_some() => { - let this_ptr = &mut self - .get_indexed_mut(state, lib, obj, is_ref, idx_val, pos, op_pos, true)?; + let mut idx_val2 = idx_val.clone(); - this_ptr.set_value(new_val.unwrap(), rhs.position())?; - Ok((Default::default(), true)) + match self.get_indexed_mut(state, lib, target, idx_val, pos, true) { + // Indexed value is an owned value - the only possibility is an indexer + // Try to call an index setter + Ok(this_ptr) if this_ptr.is_value() => { + let fn_name = FUNC_INDEXER_SET; + let args = &mut [target.as_mut(), &mut idx_val2, &mut new_val.unwrap()]; + + self.exec_fn_call(state, lib, fn_name, true, 0, args, is_ref, None, 0) + .or_else(|err| match *err { + // If there is no index setter, no need to set it back because the indexer is read-only + EvalAltResult::ErrorFunctionNotFound(s, _) + if s == FUNC_INDEXER_SET => + { + Ok(Default::default()) + } + _ => Err(err), + })?; + } + // Indexed value is a reference - update directly + Ok(ref mut this_ptr) => { + this_ptr + .set_value(new_val.unwrap()) + .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; + } + Err(err) => match *err { + // No index getter - try to call an index setter + EvalAltResult::ErrorIndexingType(_, _) => { + let fn_name = FUNC_INDEXER_SET; + let args = + &mut [target.as_mut(), &mut idx_val2, &mut new_val.unwrap()]; + + self.exec_fn_call( + state, lib, fn_name, true, 0, args, is_ref, None, 0, + )?; + } + // Error + err => return Err(Box::new(err)), + }, + } + Ok(Default::default()) } // xxx[rhs] _ => self - .get_indexed_mut(state, lib, obj, is_ref, idx_val, pos, op_pos, false) + .get_indexed_mut(state, lib, target, idx_val, pos, false) .map(|v| (v.clone_into_dynamic(), false)), } } else { @@ -989,98 +1098,112 @@ impl Engine { let ((name, native, pos), _, hash, _, def_val) = x.as_ref(); let def_val = def_val.as_ref(); - let mut arg_values: StaticVec<_> = once(obj) - .chain( - idx_val - .downcast_mut::>() - .unwrap() - .iter_mut(), - ) - .collect(); - let args = arg_values.as_mut(); + // Get a reference to the mutation target Dynamic + let (result, updated) = { + let obj = target.as_mut(); + let mut arg_values: StaticVec<_> = once(obj) + .chain( + idx_val + .downcast_mut::>() + .unwrap() + .iter_mut(), + ) + .collect(); + let args = arg_values.as_mut(); - self.exec_fn_call( - state, lib, name, *native, *hash, args, is_ref, def_val, *pos, 0, - ) + self.exec_fn_call( + state, lib, name, *native, *hash, args, is_ref, def_val, 0, + ) + .map_err(|err| EvalAltResult::new_position(err, *pos))? + }; + + // Feed the changed temp value back + if updated && !is_ref && !is_value { + let new_val = target.as_mut().clone(); + target.set_value(new_val)?; + } + + Ok((result, updated)) } // xxx.module::fn_name(...) - syntax error Expr::FnCall(_) => unreachable!(), // {xxx:map}.id = ??? #[cfg(not(feature = "no_object"))] - Expr::Property(x) if obj.is::() && new_val.is_some() => { + Expr::Property(x) if target.is::() && new_val.is_some() => { let ((prop, _, _), pos) = x.as_ref(); let index = prop.clone().into(); - let mut val = - self.get_indexed_mut(state, lib, obj, is_ref, index, *pos, op_pos, true)?; + let mut val = self.get_indexed_mut(state, lib, target, index, *pos, true)?; - val.set_value(new_val.unwrap(), rhs.position())?; + val.set_value(new_val.unwrap()) + .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; Ok((Default::default(), true)) } // {xxx:map}.id #[cfg(not(feature = "no_object"))] - Expr::Property(x) if obj.is::() => { + Expr::Property(x) if target.is::() => { let ((prop, _, _), pos) = x.as_ref(); let index = prop.clone().into(); - let val = - self.get_indexed_mut(state, lib, obj, is_ref, index, *pos, op_pos, false)?; + let val = self.get_indexed_mut(state, lib, target, index, *pos, false)?; Ok((val.clone_into_dynamic(), false)) } // xxx.id = ??? Expr::Property(x) if new_val.is_some() => { let ((_, _, setter), pos) = x.as_ref(); - let mut args = [obj, new_val.as_mut().unwrap()]; - self.exec_fn_call( - state, lib, setter, true, 0, &mut args, is_ref, None, *pos, 0, - ) - .map(|(v, _)| (v, true)) + let mut args = [target.as_mut(), new_val.as_mut().unwrap()]; + self.exec_fn_call(state, lib, setter, true, 0, &mut args, is_ref, None, 0) + .map(|(v, _)| (v, true)) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } // xxx.id Expr::Property(x) => { let ((_, getter, _), pos) = x.as_ref(); - let mut args = [obj]; - self.exec_fn_call( - state, lib, getter, true, 0, &mut args, is_ref, None, *pos, 0, - ) - .map(|(v, _)| (v, false)) + let mut args = [target.as_mut()]; + self.exec_fn_call(state, lib, getter, true, 0, &mut args, is_ref, None, 0) + .map(|(v, _)| (v, false)) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } #[cfg(not(feature = "no_object"))] // {xxx:map}.prop[expr] | {xxx:map}.prop.expr - Expr::Index(x) | Expr::Dot(x) if obj.is::() => { + Expr::Index(x) | Expr::Dot(x) if target.is::() => { let (prop, expr, pos) = x.as_ref(); let is_idx = matches!(rhs, Expr::Index(_)); let mut val = if let Expr::Property(p) = prop { let ((prop, _, _), _) = p.as_ref(); let index = prop.clone().into(); - self.get_indexed_mut(state, lib, obj, is_ref, index, *pos, op_pos, false)? + self.get_indexed_mut(state, lib, target, index, *pos, false)? } else { unreachable!(); }; self.eval_dot_index_chain_helper( - state, lib, &mut val, expr, idx_values, is_idx, *pos, level, new_val, + state, lib, &mut val, expr, idx_values, is_idx, level, new_val, ) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } // xxx.prop[expr] | xxx.prop.expr Expr::Index(x) | Expr::Dot(x) => { let (prop, expr, pos) = x.as_ref(); let is_idx = matches!(rhs, Expr::Index(_)); - let args = &mut [obj, &mut Default::default()]; + let args = &mut [target.as_mut(), &mut Default::default()]; let (mut val, updated) = if let Expr::Property(p) = prop { let ((_, getter, _), _) = p.as_ref(); let args = &mut args[..1]; - self.exec_fn_call(state, lib, getter, true, 0, args, is_ref, None, *pos, 0)? + self.exec_fn_call(state, lib, getter, true, 0, args, is_ref, None, 0) + .map_err(|err| EvalAltResult::new_position(err, *pos))? } else { unreachable!(); }; let val = &mut val; let target = &mut val.into(); - let (result, may_be_changed) = self.eval_dot_index_chain_helper( - state, lib, target, expr, idx_values, is_idx, *pos, level, new_val, - )?; + let (result, may_be_changed) = self + .eval_dot_index_chain_helper( + state, lib, target, expr, idx_values, is_idx, level, new_val, + ) + .map_err(|err| EvalAltResult::new_position(err, *pos))?; // Feed the value back via a setter just in case it has been updated if updated || may_be_changed { @@ -1088,14 +1211,12 @@ impl Engine { let ((_, _, setter), _) = p.as_ref(); // Re-use args because the first &mut parameter will not be consumed args[1] = val; - self.exec_fn_call( - state, lib, setter, true, 0, args, is_ref, None, *pos, 0, - ) - .or_else(|err| match *err { - // If there is no setter, no need to feed it back because the property is read-only - EvalAltResult::ErrorDotExpr(_, _) => Ok(Default::default()), - err => Err(Box::new(err)), - })?; + self.exec_fn_call(state, lib, setter, true, 0, args, is_ref, None, 0) + .or_else(|err| match *err { + // If there is no setter, no need to feed it back because the property is read-only + EvalAltResult::ErrorDotExpr(_, _) => Ok(Default::default()), + err => Err(EvalAltResult::new_position(Box::new(err), *pos)), + })?; } } @@ -1115,7 +1236,7 @@ impl Engine { &self, scope: &mut Scope, state: &mut State, - lib: &FunctionsLib, + lib: &Module, expr: &Expr, level: usize, new_val: Option, @@ -1134,7 +1255,8 @@ impl Engine { // id.??? or id[???] Expr::Variable(_) => { let (target, name, typ, pos) = search_scope(scope, state, dot_lhs)?; - self.inc_operations(state, pos)?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, pos))?; // Constants cannot be modified match typ { @@ -1150,9 +1272,10 @@ impl Engine { let this_ptr = &mut target.into(); self.eval_dot_index_chain_helper( - state, lib, this_ptr, dot_rhs, idx_values, is_index, *op_pos, level, new_val, + state, lib, this_ptr, dot_rhs, idx_values, is_index, level, new_val, ) .map(|(v, _)| v) + .map_err(|err| EvalAltResult::new_position(err, *op_pos)) } // {expr}.??? = ??? or {expr}[???] = ??? expr if new_val.is_some() => { @@ -1165,9 +1288,10 @@ impl Engine { let val = self.eval_expr(scope, state, lib, expr, level)?; let this_ptr = &mut val.into(); self.eval_dot_index_chain_helper( - state, lib, this_ptr, dot_rhs, idx_values, is_index, *op_pos, level, new_val, + state, lib, this_ptr, dot_rhs, idx_values, is_index, level, new_val, ) .map(|(v, _)| v) + .map_err(|err| EvalAltResult::new_position(err, *op_pos)) } } } @@ -1181,13 +1305,14 @@ impl Engine { &self, scope: &mut Scope, state: &mut State, - lib: &FunctionsLib, + lib: &Module, expr: &Expr, idx_values: &mut StaticVec, size: usize, level: usize, ) -> Result<(), Box> { - self.inc_operations(state, expr.position())?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, expr.position()))?; match expr { Expr::FnCall(x) if x.1.is_none() => { @@ -1221,18 +1346,20 @@ impl Engine { } /// Get the value at the indexed position of a base type + /// Position in `EvalAltResult` may be None and should be set afterwards. fn get_indexed_mut<'a>( &self, state: &mut State, - lib: &FunctionsLib, - val: &'a mut Dynamic, - is_ref: bool, + lib: &Module, + target: &'a mut Target, mut idx: Dynamic, idx_pos: Position, - op_pos: Position, create: bool, ) -> Result, Box> { - self.inc_operations(state, op_pos)?; + self.inc_operations(state)?; + + let is_ref = target.is_ref(); + let val = target.as_mut(); match val { #[cfg(not(feature = "no_index"))] @@ -1298,16 +1425,26 @@ impl Engine { } } + #[cfg(not(feature = "no_index"))] _ => { - let fn_name = FUNC_INDEXER; + let fn_name = FUNC_INDEXER_GET; let type_name = self.map_type_name(val.type_name()); let args = &mut [val, &mut idx]; - self.exec_fn_call(state, lib, fn_name, true, 0, args, is_ref, None, op_pos, 0) + self.exec_fn_call(state, lib, fn_name, true, 0, args, is_ref, None, 0) .map(|(v, _)| v.into()) .map_err(|_| { - Box::new(EvalAltResult::ErrorIndexingType(type_name.into(), op_pos)) + Box::new(EvalAltResult::ErrorIndexingType( + type_name.into(), + Position::none(), + )) }) } + + #[cfg(feature = "no_index")] + _ => Err(Box::new(EvalAltResult::ErrorIndexingType( + self.map_type_name(val.type_name()).into(), + Position::none(), + ))), } } @@ -1316,12 +1453,13 @@ impl Engine { &self, scope: &mut Scope, state: &mut State, - lib: &FunctionsLib, + lib: &Module, lhs: &Expr, rhs: &Expr, level: usize, ) -> Result> { - self.inc_operations(state, rhs.position())?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; let lhs_value = self.eval_expr(scope, state, lib, lhs, level)?; let rhs_value = self.eval_expr(scope, state, lib, rhs, level)?; @@ -1337,7 +1475,6 @@ impl Engine { for value in rhs_value.iter_mut() { let args = &mut [&mut lhs_value.clone(), value]; let def_value = Some(&def_value); - let pos = rhs.position(); let hashes = ( // Qualifiers (none) + function name + number of arguments + argument `TypeId`'s. @@ -1345,9 +1482,11 @@ impl Engine { 0, ); - let (r, _) = self.call_fn_raw( - &mut scope, state, lib, op, hashes, args, false, def_value, pos, level, - )?; + let (r, _) = self + .call_fn_raw( + &mut scope, state, lib, op, hashes, args, false, def_value, level, + ) + .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; if r.as_bool().unwrap_or(false) { return Ok(true.into()); } @@ -1377,13 +1516,14 @@ impl Engine { &self, scope: &mut Scope, state: &mut State, - lib: &FunctionsLib, + lib: &Module, expr: &Expr, level: usize, ) -> Result> { - self.inc_operations(state, expr.position())?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, expr.position()))?; - match expr { + let result = match expr { Expr::Expr(x) => self.eval_expr(scope, state, lib, x.as_ref(), level), Expr::IntegerConstant(x) => Ok(x.0.into()), @@ -1398,14 +1538,15 @@ impl Engine { Expr::Property(_) => unreachable!(), // Statement block - Expr::Stmt(stmt) => self.eval_stmt(scope, state, lib, &stmt.0, level), + Expr::Stmt(x) => self.eval_stmt(scope, state, lib, &x.0, level), // var op= rhs Expr::Assignment(x) if matches!(x.0, Expr::Variable(_)) => { let (lhs_expr, op, rhs_expr, op_pos) = x.as_ref(); let mut rhs_val = self.eval_expr(scope, state, lib, rhs_expr, level)?; let (lhs_ptr, name, typ, pos) = search_scope(scope, state, lhs_expr)?; - self.inc_operations(state, pos)?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, pos))?; match typ { // Assignment to constant variable @@ -1433,7 +1574,7 @@ impl Engine { .or_else(|| self.packages.get_fn(hash_fn)) { // Overriding exact implementation - func(&mut [lhs_ptr, &mut rhs_val])?; + func(self, &mut [lhs_ptr, &mut rhs_val])?; } else if run_builtin_op_assignment(op, lhs_ptr, &rhs_val)?.is_none() { // Not built in, map to `var = var op rhs` let op = &op[..op.len() - 1]; // extract operator without = @@ -1442,10 +1583,9 @@ impl Engine { // Set variable value *lhs_ptr = self - .exec_fn_call( - state, lib, op, true, hash, args, false, None, *op_pos, level, - ) - .map(|(v, _)| v)?; + .exec_fn_call(state, lib, op, true, hash, args, false, None, level) + .map(|(v, _)| v) + .map_err(|err| EvalAltResult::new_position(err, *op_pos))?; } Ok(Default::default()) } @@ -1470,10 +1610,9 @@ impl Engine { &mut self.eval_expr(scope, state, lib, lhs_expr, level)?, &mut rhs_val, ]; - self.exec_fn_call( - state, lib, op, true, hash, args, false, None, *op_pos, level, - ) - .map(|(v, _)| v)? + self.exec_fn_call(state, lib, op, true, hash, args, false, None, level) + .map(|(v, _)| v) + .map_err(|err| EvalAltResult::new_position(err, *op_pos))? }); match lhs_expr { @@ -1541,11 +1680,11 @@ impl Engine { if !self.has_override(lib, (hash_fn, *hash)) { // eval - only in function call style let prev_len = scope.len(); - let pos = args_expr.get(0).position(); - - // Evaluate the text string as a script - let script = self.eval_expr(scope, state, lib, args_expr.get(0), level)?; - let result = self.eval_script_expr(scope, state, lib, &script, pos); + let expr = args_expr.get(0); + let script = self.eval_expr(scope, state, lib, expr, level)?; + let result = self + .eval_script_expr(scope, state, lib, &script) + .map_err(|err| EvalAltResult::new_position(err, expr.position())); if scope.len() != prev_len { // IMPORTANT! If the eval defines new variables in the current scope, @@ -1578,7 +1717,8 @@ impl Engine { .collect::>()?; let (target, _, typ, pos) = search_scope(scope, state, lhs)?; - self.inc_operations(state, pos)?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, pos))?; match typ { ScopeEntryType::Module => unreachable!(), @@ -1603,15 +1743,15 @@ impl Engine { let args = args.as_mut(); self.exec_fn_call( - state, lib, name, *native, *hash, args, is_ref, def_val, *pos, level, + state, lib, name, *native, *hash, args, is_ref, def_val, level, ) .map(|(v, _)| v) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } // Module-qualified function call - #[cfg(not(feature = "no_module"))] Expr::FnCall(x) if x.1.is_some() => { - let ((name, _, pos), modules, hash_fn_def, args_expr, def_val) = x.as_ref(); + let ((name, _, pos), modules, hash_script, args_expr, def_val) = x.as_ref(); let modules = modules.as_ref().unwrap(); let mut arg_values = args_expr @@ -1630,18 +1770,19 @@ impl Engine { .downcast_mut::() .unwrap() } else { - scope.find_module(id).ok_or_else(|| { + scope.find_module_internal(id).ok_or_else(|| { Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)) })? }; // First search in script-defined functions (can override built-in) - let func = match module.get_qualified_fn(name, *hash_fn_def) { + let func = match module.get_qualified_fn(name, *hash_script) { Err(err) if matches!(*err, EvalAltResult::ErrorFunctionNotFound(_, _)) => { // Then search in Rust functions - self.inc_operations(state, *pos)?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, *pos))?; - // Rust functions are indexed in two steps: + // Qualified Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, // i.e. qualifiers + function name + number of arguments. // 2) Calculate a second hash with no qualifiers, empty function name, @@ -1649,21 +1790,24 @@ impl Engine { let hash_fn_args = calc_fn_hash(empty(), "", 0, args.iter().map(|a| a.type_id())); // 3) The final hash is the XOR of the two hashes. - let hash_fn_native = *hash_fn_def ^ hash_fn_args; + let hash_qualified_fn = *hash_script ^ hash_fn_args; - module.get_qualified_fn(name, hash_fn_native) + module.get_qualified_fn(name, hash_qualified_fn) } r => r, }; match func { - Ok(x) if x.is_script() => { + Ok(f) if f.is_script() => { let args = args.as_mut(); - let fn_def = x.get_fn_def(); + let fn_def = f.get_fn_def(); let mut scope = Scope::new(); - self.call_script_fn(&mut scope, state, lib, name, fn_def, args, *pos, level) + self.call_script_fn(&mut scope, state, lib, name, fn_def, args, level) + .map_err(|err| EvalAltResult::new_position(err, *pos)) + } + Ok(f) => { + f.get_native_fn()(self, args.as_mut()).map_err(|err| err.new_position(*pos)) } - Ok(x) => x.get_native_fn()(args.as_mut()).map_err(|err| err.new_position(*pos)), Err(err) if def_val.is_some() && matches!(*err, EvalAltResult::ErrorFunctionNotFound(_, _)) => @@ -1717,7 +1861,9 @@ impl Engine { Expr::Unit(_) => Ok(().into()), _ => unreachable!(), - } + }; + + self.check_data_size(result) } /// Evaluate a statement @@ -1725,13 +1871,14 @@ impl Engine { &self, scope: &mut Scope, state: &mut State, - lib: &FunctionsLib, + lib: &Module, stmt: &Stmt, level: usize, ) -> Result> { - self.inc_operations(state, stmt.position())?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, stmt.position()))?; - match stmt { + let result = match stmt { // No-op Stmt::Noop(_) => Ok(Default::default()), @@ -1834,7 +1981,8 @@ impl Engine { for loop_var in func(iter_type) { *scope.get_mut(index).0 = loop_var; - self.inc_operations(state, stmt.position())?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, stmt.position()))?; match self.eval_stmt(scope, state, lib, stmt, level) { Ok(_) => (), @@ -1919,21 +2067,18 @@ impl Engine { // Import statement Stmt::Import(x) => { - #[cfg(feature = "no_module")] - unreachable!(); + let (expr, (name, pos)) = x.as_ref(); - #[cfg(not(feature = "no_module"))] + // Guard against too many modules + if state.modules >= self.max_modules { + return Err(Box::new(EvalAltResult::ErrorTooManyModules(*pos))); + } + + if let Some(path) = self + .eval_expr(scope, state, lib, &expr, level)? + .try_cast::() { - let (expr, (name, pos)) = x.as_ref(); - - // Guard against too many modules - if state.modules >= self.max_modules { - return Err(Box::new(EvalAltResult::ErrorTooManyModules(*pos))); - } - - if let Some(path) = self - .eval_expr(scope, state, lib, &expr, level)? - .try_cast::() + #[cfg(not(feature = "no_module"))] { if let Some(resolver) = &self.module_resolver { // Use an empty scope to create a module @@ -1941,7 +2086,7 @@ impl Engine { resolver.resolve(self, Scope::new(), &path, expr.position())?; let mod_name = unsafe_cast_var_name_to_lifetime(name, &state); - scope.push_module(mod_name, module); + scope.push_module_internal(mod_name, module); state.modules += 1; @@ -1952,9 +2097,12 @@ impl Engine { expr.position(), ))) } - } else { - Err(Box::new(EvalAltResult::ErrorImportExpr(expr.position()))) } + + #[cfg(feature = "no_module")] + Ok(Default::default()) + } else { + Err(Box::new(EvalAltResult::ErrorImportExpr(expr.position()))) } } @@ -1982,26 +2130,127 @@ impl Engine { } Ok(Default::default()) } + }; + + self.check_data_size(result) + } + + /// Check a result to ensure that the data size is within allowable limit. + fn check_data_size( + &self, + result: Result>, + ) -> Result> { + #[cfg(feature = "unchecked")] + return result; + + // If no data size limits, just return + if self.max_string_size + self.max_array_size + self.max_map_size == 0 { + return result; + } + + // Recursively calculate the size of a value (especially `Array` and `Map`) + fn calc_size(value: &Dynamic) -> (usize, usize, usize) { + match value { + #[cfg(not(feature = "no_index"))] + Dynamic(Union::Array(arr)) => { + let mut arrays = 0; + let mut maps = 0; + + arr.iter().for_each(|value| match value { + Dynamic(Union::Array(_)) | Dynamic(Union::Map(_)) => { + let (a, m, _) = calc_size(value); + arrays += a; + maps += m; + } + _ => arrays += 1, + }); + + (arrays, maps, 0) + } + #[cfg(not(feature = "no_object"))] + Dynamic(Union::Map(map)) => { + let mut arrays = 0; + let mut maps = 0; + + map.values().for_each(|value| match value { + Dynamic(Union::Array(_)) | Dynamic(Union::Map(_)) => { + let (a, m, _) = calc_size(value); + arrays += a; + maps += m; + } + _ => maps += 1, + }); + + (arrays, maps, 0) + } + Dynamic(Union::Str(s)) => (0, 0, s.len()), + _ => (0, 0, 0), + } + } + + match result { + // Simply return all errors + Err(_) => return result, + // String with limit + Ok(Dynamic(Union::Str(_))) if self.max_string_size > 0 => (), + // Array with limit + #[cfg(not(feature = "no_index"))] + Ok(Dynamic(Union::Array(_))) if self.max_array_size > 0 => (), + // Map with limit + #[cfg(not(feature = "no_object"))] + Ok(Dynamic(Union::Map(_))) if self.max_map_size > 0 => (), + // Everything else is simply returned + Ok(_) => return result, + }; + + let (arr, map, s) = calc_size(result.as_ref().unwrap()); + + if s > self.max_string_size { + Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Length of string".to_string(), + self.max_string_size, + s, + Position::none(), + ))) + } else if arr > self.max_array_size { + Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Size of array".to_string(), + self.max_array_size, + arr, + Position::none(), + ))) + } else if map > self.max_map_size { + Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Number of properties in object map".to_string(), + self.max_map_size, + map, + Position::none(), + ))) + } else { + result } } /// Check if the number of operations stay within limit. - fn inc_operations(&self, state: &mut State, pos: Position) -> Result<(), Box> { + /// Position in `EvalAltResult` is None and must be set afterwards. + fn inc_operations(&self, state: &mut State) -> Result<(), Box> { state.operations += 1; #[cfg(not(feature = "unchecked"))] { // Guard against too many operations - if state.operations > self.max_operations { - return Err(Box::new(EvalAltResult::ErrorTooManyOperations(pos))); + if self.max_operations > 0 && state.operations > self.max_operations { + return Err(Box::new(EvalAltResult::ErrorTooManyOperations( + Position::none(), + ))); } } // Report progress - only in steps if let Some(progress) = &self.progress { - if !progress(state.operations) { + if !progress(&state.operations) { // Terminate script if progress returns false - return Err(Box::new(EvalAltResult::ErrorTerminated(pos))); + return Err(Box::new(EvalAltResult::ErrorTerminated(Position::none()))); } } @@ -2037,14 +2286,14 @@ fn run_builtin_binary_op( #[cfg(not(feature = "unchecked"))] match op { - "+" => return add(x, y).map(Into::::into).map(Some), - "-" => return sub(x, y).map(Into::::into).map(Some), - "*" => return mul(x, y).map(Into::::into).map(Some), - "/" => return div(x, y).map(Into::::into).map(Some), - "%" => return modulo(x, y).map(Into::::into).map(Some), - "~" => return pow_i_i(x, y).map(Into::::into).map(Some), - ">>" => return shr(x, y).map(Into::::into).map(Some), - "<<" => return shl(x, y).map(Into::::into).map(Some), + "+" => return add(x, y).map(Into::into).map(Some), + "-" => return sub(x, y).map(Into::into).map(Some), + "*" => return mul(x, y).map(Into::into).map(Some), + "/" => return div(x, y).map(Into::into).map(Some), + "%" => return modulo(x, y).map(Into::into).map(Some), + "~" => return pow_i_i(x, y).map(Into::into).map(Some), + ">>" => return shr(x, y).map(Into::into).map(Some), + "<<" => return shl(x, y).map(Into::into).map(Some), _ => (), } @@ -2055,9 +2304,9 @@ fn run_builtin_binary_op( "*" => return Ok(Some((x * y).into())), "/" => return Ok(Some((x / y).into())), "%" => return Ok(Some((x % y).into())), - "~" => return pow_i_i_u(x, y).map(Into::::into).map(Some), - ">>" => return shr_u(x, y).map(Into::::into).map(Some), - "<<" => return shl_u(x, y).map(Into::::into).map(Some), + "~" => return pow_i_i_u(x, y).map(Into::into).map(Some), + ">>" => return shr_u(x, y).map(Into::into).map(Some), + "<<" => return shl_u(x, y).map(Into::into).map(Some), _ => (), } @@ -2131,7 +2380,7 @@ fn run_builtin_binary_op( "*" => return Ok(Some((x * y).into())), "/" => return Ok(Some((x / y).into())), "%" => return Ok(Some((x % y).into())), - "~" => return pow_f_f(x, y).map(Into::::into).map(Some), + "~" => return pow_f_f(x, y).map(Into::into).map(Some), "==" => return Ok(Some((x == y).into())), "!=" => return Ok(Some((x != y).into())), ">" => return Ok(Some((x > y).into())), diff --git a/src/error.rs b/src/error.rs index 7bc60a1c..ce8615ed 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,6 @@ //! Module containing error definitions for the parsing process. +use crate::result::EvalAltResult; use crate::token::Position; use crate::stdlib::{boxed::Box, char, error::Error, fmt, string::String}; @@ -11,6 +12,8 @@ pub enum LexError { UnexpectedChar(char), /// A string literal is not terminated before a new-line or EOF. UnterminatedString, + /// An identifier is in an invalid format. + StringTooLong(usize), /// An string/character/numeric escape sequence is in an invalid format. MalformedEscapeSequence(String), /// An numeric literal is in an invalid format. @@ -19,8 +22,8 @@ pub enum LexError { MalformedChar(String), /// An identifier is in an invalid format. MalformedIdentifier(String), - /// Bad keyword encountered when tokenizing the script text. - ImproperKeyword(String), + /// Bad symbol encountered when tokenizing the script text. + ImproperSymbol(String), } impl Error for LexError {} @@ -34,11 +37,23 @@ impl fmt::Display for LexError { Self::MalformedChar(s) => write!(f, "Invalid character: '{}'", s), Self::MalformedIdentifier(s) => write!(f, "Variable name is not proper: '{}'", s), Self::UnterminatedString => write!(f, "Open string is not terminated"), - Self::ImproperKeyword(s) => write!(f, "{}", s), + Self::StringTooLong(max) => write!( + f, + "Length of string literal exceeds the maximum limit ({})", + max + ), + Self::ImproperSymbol(s) => write!(f, "{}", s), } } } +impl LexError { + /// Convert a `LexError` into a `ParseError`. + pub fn into_err(&self, pos: Position) -> ParseError { + ParseError(Box::new(self.into()), pos) + } +} + /// Type of error encountered when parsing a script. /// /// Some errors never appear when certain features are turned on. @@ -108,12 +123,16 @@ pub enum ParseErrorType { WrongExport, /// Assignment to a copy of a value. AssignmentToCopy, - /// Assignment to an a constant variable. + /// Assignment to an a constant variable. Wrapped value is the constant variable name. AssignmentToConstant(String), /// Expression exceeding the maximum levels of complexity. /// /// Never appears under the `unchecked` feature. ExprTooDeep, + /// Literal exceeding the maximum size. Wrapped values are the data type name and the maximum size. + /// + /// Never appears under the `unchecked` feature. + LiteralTooLarge(String, usize), /// Break statement not inside a loop. LoopBreak, } @@ -123,113 +142,126 @@ impl ParseErrorType { pub(crate) fn into_err(self, pos: Position) -> ParseError { ParseError(Box::new(self), pos) } + + pub(crate) fn desc(&self) -> &str { + match self { + Self::BadInput(p) => p, + Self::UnexpectedEOF => "Script is incomplete", + Self::UnknownOperator(_) => "Unknown operator", + Self::MissingToken(_, _) => "Expecting a certain token that is missing", + Self::MalformedCallExpr(_) => "Invalid expression in function call arguments", + Self::MalformedIndexExpr(_) => "Invalid index in indexing expression", + Self::MalformedInExpr(_) => "Invalid 'in' expression", + Self::DuplicatedProperty(_) => "Duplicated property in object map literal", + Self::ForbiddenConstantExpr(_) => "Expecting a constant", + Self::PropertyExpected => "Expecting name of a property", + Self::VariableExpected => "Expecting name of a variable", + Self::ExprExpected(_) => "Expecting an expression", + Self::FnMissingName => "Expecting name in function declaration", + Self::FnMissingParams(_) => "Expecting parameters in function declaration", + Self::FnDuplicatedParam(_,_) => "Duplicated parameters in function declaration", + Self::FnMissingBody(_) => "Expecting body statement block for function declaration", + Self::WrongFnDefinition => "Function definitions must be at global level and cannot be inside a block or another function", + Self::DuplicatedExport(_) => "Duplicated variable/function in export statement", + Self::WrongExport => "Export statement can only appear at global level", + Self::AssignmentToCopy => "Only a copy of the value is change with this assignment", + Self::AssignmentToConstant(_) => "Cannot assign to a constant value", + Self::ExprTooDeep => "Expression exceeds maximum complexity", + Self::LiteralTooLarge(_, _) => "Literal exceeds maximum limit", + Self::LoopBreak => "Break statement should only be used inside a loop" + } + } +} + +impl fmt::Display for ParseErrorType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BadInput(s) | ParseErrorType::MalformedCallExpr(s) => { + write!(f, "{}", if s.is_empty() { self.desc() } else { s }) + } + Self::ForbiddenConstantExpr(s) => { + write!(f, "Expecting a constant to assign to '{}'", s) + } + Self::UnknownOperator(s) => write!(f, "{}: '{}'", self.desc(), s), + + Self::MalformedIndexExpr(s) => { + write!(f, "{}", if s.is_empty() { self.desc() } else { s }) + } + + Self::MalformedInExpr(s) => write!(f, "{}", if s.is_empty() { self.desc() } else { s }), + + Self::DuplicatedProperty(s) => { + write!(f, "Duplicated property '{}' for object map literal", s) + } + + Self::ExprExpected(s) => write!(f, "Expecting {} expression", s), + + Self::FnMissingParams(s) => write!(f, "Expecting parameters for function '{}'", s), + + Self::FnMissingBody(s) => { + write!(f, "Expecting body statement block for function '{}'", s) + } + + Self::FnDuplicatedParam(s, arg) => { + write!(f, "Duplicated parameter '{}' for function '{}'", arg, s) + } + + Self::DuplicatedExport(s) => write!( + f, + "Duplicated variable/function '{}' in export statement", + s + ), + + Self::MissingToken(token, s) => write!(f, "Expecting '{}' {}", token, s), + + Self::AssignmentToConstant(s) if s.is_empty() => write!(f, "{}", self.desc()), + Self::AssignmentToConstant(s) => write!(f, "Cannot assign to constant '{}'", s), + Self::LiteralTooLarge(typ, max) => { + write!(f, "{} exceeds the maximum limit ({})", typ, max) + } + _ => write!(f, "{}", self.desc()), + } + } +} + +impl From<&LexError> for ParseErrorType { + fn from(err: &LexError) -> Self { + match err { + LexError::StringTooLong(max) => { + Self::LiteralTooLarge("Length of string literal".to_string(), *max) + } + _ => Self::BadInput(err.to_string()), + } + } } /// Error when parsing a script. #[derive(Debug, Eq, PartialEq, Clone, Hash)] -pub struct ParseError(pub(crate) Box, pub(crate) Position); - -impl ParseError { - /// Get the parse error. - pub fn error_type(&self) -> &ParseErrorType { - &self.0 - } - - /// Get the location in the script of the error. - pub fn position(&self) -> Position { - self.1 - } - - pub(crate) fn desc(&self) -> &str { - match self.0.as_ref() { - ParseErrorType::BadInput(p) => p, - ParseErrorType::UnexpectedEOF => "Script is incomplete", - ParseErrorType::UnknownOperator(_) => "Unknown operator", - ParseErrorType::MissingToken(_, _) => "Expecting a certain token that is missing", - ParseErrorType::MalformedCallExpr(_) => "Invalid expression in function call arguments", - ParseErrorType::MalformedIndexExpr(_) => "Invalid index in indexing expression", - ParseErrorType::MalformedInExpr(_) => "Invalid 'in' expression", - ParseErrorType::DuplicatedProperty(_) => "Duplicated property in object map literal", - ParseErrorType::ForbiddenConstantExpr(_) => "Expecting a constant", - ParseErrorType::PropertyExpected => "Expecting name of a property", - ParseErrorType::VariableExpected => "Expecting name of a variable", - ParseErrorType::ExprExpected(_) => "Expecting an expression", - ParseErrorType::FnMissingName => "Expecting name in function declaration", - ParseErrorType::FnMissingParams(_) => "Expecting parameters in function declaration", - ParseErrorType::FnDuplicatedParam(_,_) => "Duplicated parameters in function declaration", - ParseErrorType::FnMissingBody(_) => "Expecting body statement block for function declaration", - ParseErrorType::WrongFnDefinition => "Function definitions must be at global level and cannot be inside a block or another function", - ParseErrorType::DuplicatedExport(_) => "Duplicated variable/function in export statement", - ParseErrorType::WrongExport => "Export statement can only appear at global level", - ParseErrorType::AssignmentToCopy => "Only a copy of the value is change with this assignment", - ParseErrorType::AssignmentToConstant(_) => "Cannot assign to a constant value", - ParseErrorType::ExprTooDeep => "Expression exceeds maximum complexity", - ParseErrorType::LoopBreak => "Break statement should only be used inside a loop" - } - } -} +pub struct ParseError(pub Box, pub Position); impl Error for ParseError {} impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.0.as_ref() { - ParseErrorType::BadInput(s) | ParseErrorType::MalformedCallExpr(s) => { - write!(f, "{}", if s.is_empty() { self.desc() } else { s })? - } - ParseErrorType::ForbiddenConstantExpr(s) => { - write!(f, "Expecting a constant to assign to '{}'", s)? - } - ParseErrorType::UnknownOperator(s) => write!(f, "{}: '{}'", self.desc(), s)?, - - ParseErrorType::MalformedIndexExpr(s) => { - write!(f, "{}", if s.is_empty() { self.desc() } else { s })? - } - - ParseErrorType::MalformedInExpr(s) => { - write!(f, "{}", if s.is_empty() { self.desc() } else { s })? - } - - ParseErrorType::DuplicatedProperty(s) => { - write!(f, "Duplicated property '{}' for object map literal", s)? - } - - ParseErrorType::ExprExpected(s) => write!(f, "Expecting {} expression", s)?, - - ParseErrorType::FnMissingParams(s) => { - write!(f, "Expecting parameters for function '{}'", s)? - } - - ParseErrorType::FnMissingBody(s) => { - write!(f, "Expecting body statement block for function '{}'", s)? - } - - ParseErrorType::FnDuplicatedParam(s, arg) => { - write!(f, "Duplicated parameter '{}' for function '{}'", arg, s)? - } - - ParseErrorType::DuplicatedExport(s) => write!( - f, - "Duplicated variable/function '{}' in export statement", - s - )?, - - ParseErrorType::MissingToken(token, s) => write!(f, "Expecting '{}' {}", token, s)?, - - ParseErrorType::AssignmentToConstant(s) if s.is_empty() => { - write!(f, "{}", self.desc())? - } - ParseErrorType::AssignmentToConstant(s) => { - write!(f, "Cannot assign to constant '{}'", s)? - } - _ => write!(f, "{}", self.desc())?, - } + fmt::Display::fmt(&self.0, f)?; + // Do not write any position if None if !self.1.is_none() { - // Do not write any position if None - Ok(()) - } else { - write!(f, " ({})", self.1) + write!(f, " ({})", self.1)?; } + + Ok(()) + } +} + +impl From for Box { + fn from(err: ParseErrorType) -> Self { + Box::new(EvalAltResult::ErrorParsing(err, Position::none())) + } +} + +impl From for Box { + fn from(err: ParseError) -> Self { + Box::new(EvalAltResult::ErrorParsing(*err.0, err.1)) } } diff --git a/src/fn_native.rs b/src/fn_native.rs index 6c43adc6..6c9eba10 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -1,8 +1,9 @@ use crate::any::Dynamic; -use crate::parser::FnDef; +use crate::engine::Engine; +use crate::parser::ScriptFnDef; use crate::result::EvalAltResult; -use crate::stdlib::{boxed::Box, rc::Rc, sync::Arc}; +use crate::stdlib::{boxed::Box, fmt, rc::Rc, sync::Arc}; #[cfg(feature = "sync")] pub trait SendSync: Send + Sync {} @@ -51,12 +52,18 @@ pub fn shared_take(value: Shared) -> T { pub type FnCallArgs<'a> = [&'a mut Dynamic]; #[cfg(not(feature = "sync"))] -pub type FnAny = dyn Fn(&mut FnCallArgs) -> Result>; +pub type FnAny = dyn Fn(&Engine, &mut FnCallArgs) -> Result>; #[cfg(feature = "sync")] -pub type FnAny = dyn Fn(&mut FnCallArgs) -> Result> + Send + Sync; +pub type FnAny = + dyn Fn(&Engine, &mut FnCallArgs) -> Result> + Send + Sync; pub type IteratorFn = fn(Dynamic) -> Box>; +#[cfg(not(feature = "sync"))] +pub type Callback = Box R + 'static>; +#[cfg(feature = "sync")] +pub type Callback = Box R + Send + Sync + 'static>; + /// A type encapsulating a function callable by Rhai. #[derive(Clone)] pub enum CallableFunction { @@ -68,7 +75,18 @@ pub enum CallableFunction { /// An iterator function. Iterator(IteratorFn), /// A script-defined function. - Script(Shared), + Script(Shared), +} + +impl fmt::Debug for CallableFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Pure(_) => write!(f, "NativePureFunction"), + Self::Method(_) => write!(f, "NativeMethod"), + Self::Iterator(_) => write!(f, "NativeIterator"), + Self::Script(fn_def) => fmt::Debug::fmt(fn_def, f), + } + } } impl CallableFunction { @@ -108,7 +126,18 @@ impl CallableFunction { pub fn get_native_fn(&self) -> &FnAny { match self { Self::Pure(f) | Self::Method(f) => f.as_ref(), - Self::Iterator(_) | Self::Script(_) => panic!(), + Self::Iterator(_) | Self::Script(_) => unreachable!(), + } + } + /// Get a shared reference to a script-defined function definition. + /// + /// # Panics + /// + /// Panics if the `CallableFunction` is not `Script`. + pub fn get_shared_fn_def(&self) -> Shared { + match self { + Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => unreachable!(), + Self::Script(f) => f.clone(), } } /// Get a reference to a script-defined function definition. @@ -116,9 +145,9 @@ impl CallableFunction { /// # Panics /// /// Panics if the `CallableFunction` is not `Script`. - pub fn get_fn_def(&self) -> &FnDef { + pub fn get_fn_def(&self) -> &ScriptFnDef { match self { - Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => panic!(), + Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => unreachable!(), Self::Script(f) => f, } } @@ -130,7 +159,7 @@ impl CallableFunction { pub fn get_iter_fn(&self) -> IteratorFn { match self { Self::Iterator(f) => *f, - Self::Pure(_) | Self::Method(_) | Self::Script(_) => panic!(), + Self::Pure(_) | Self::Method(_) | Self::Script(_) => unreachable!(), } } /// Create a new `CallableFunction::Pure`. @@ -142,3 +171,21 @@ impl CallableFunction { Self::Method(func.into()) } } + +impl From for CallableFunction { + fn from(func: IteratorFn) -> Self { + Self::Iterator(func) + } +} + +impl From for CallableFunction { + fn from(func: ScriptFnDef) -> Self { + Self::Script(func.into()) + } +} + +impl From> for CallableFunction { + fn from(func: Shared) -> Self { + Self::Script(func) + } +} diff --git a/src/fn_register.rs b/src/fn_register.rs index cb208d12..1b07cbe8 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -1,12 +1,12 @@ //! Module which defines the function registration mechanism. - #![allow(non_snake_case)] use crate::any::{Dynamic, Variant}; use crate::engine::Engine; -use crate::fn_native::{CallableFunction, FnAny, FnCallArgs}; +use crate::fn_native::{CallableFunction, FnAny, FnCallArgs, SendSync}; use crate::parser::FnAccess; use crate::result::EvalAltResult; +use crate::utils::ImmutableString; use crate::stdlib::{any::TypeId, boxed::Box, mem}; @@ -99,9 +99,16 @@ pub fn by_ref(data: &mut Dynamic) -> &mut T { /// Dereference into value. #[inline(always)] pub fn by_value(data: &mut Dynamic) -> T { - // 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. - mem::take(data).cast::() + if TypeId::of::() == TypeId::of::<&str>() { + // If T is &str, data must be ImmutableString, so map directly to it + let ref_str = data.as_str().unwrap(); + let ref_T = unsafe { mem::transmute::<_, &T>(&ref_str) }; + ref_T.clone() + } 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. + mem::take(data).cast::() + } } /// This macro creates a closure wrapping a registered function. @@ -112,7 +119,7 @@ macro_rules! make_func { // ^ function parameter generic type name (A, B, C etc.) // ^ dereferencing function - Box::new(move |args: &mut FnCallArgs| { + Box::new(move |_: &Engine, args: &mut FnCallArgs| { // The arguments are assumed to be of the correct number and types! #[allow(unused_variables, unused_mut)] @@ -146,6 +153,18 @@ pub fn map_result( data } +/// Remap `&str` to `ImmutableString`. +#[inline(always)] +fn map_type_id() -> TypeId { + let id = TypeId::of::(); + + if id == TypeId::of::<&str>() { + TypeId::of::() + } else { + id + } +} + macro_rules! def_register { () => { def_register!(imp from_pure :); @@ -158,19 +177,13 @@ macro_rules! def_register { // ^ dereferencing function impl< $($par: Variant + Clone,)* - - #[cfg(feature = "sync")] - FN: Fn($($param),*) -> RET + Send + Sync + 'static, - - #[cfg(not(feature = "sync"))] - FN: Fn($($param),*) -> RET + 'static, - + FN: Fn($($param),*) -> RET + SendSync + 'static, RET: Variant + Clone > RegisterFn for Engine { fn register_fn(&mut self, name: &str, f: FN) { self.global_module.set_fn(name, FnAccess::Public, - &[$(TypeId::of::<$par>()),*], + &[$(map_type_id::<$par>()),*], CallableFunction::$abi(make_func!(f : map_dynamic ; $($par => $clone),*)) ); } @@ -178,16 +191,12 @@ macro_rules! def_register { impl< $($par: Variant + Clone,)* - - #[cfg(feature = "sync")] - FN: Fn($($param),*) -> Result> + Send + Sync + 'static, - #[cfg(not(feature = "sync"))] - FN: Fn($($param),*) -> Result> + 'static, + FN: Fn($($param),*) -> Result> + SendSync + 'static, > RegisterResultFn for Engine { fn register_result_fn(&mut self, name: &str, f: FN) { self.global_module.set_fn(name, FnAccess::Public, - &[$(TypeId::of::<$par>()),*], + &[$(map_type_id::<$par>()),*], CallableFunction::$abi(make_func!(f : map_result ; $($par => $clone),*)) ); } diff --git a/src/lib.rs b/src/lib.rs index 106e83d4..aeb1470c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,6 +37,7 @@ //! engine.register_fn("compute", compute_something); //! //! # #[cfg(not(feature = "no_std"))] +//! # #[cfg(not(target_arch = "wasm32"))] //! assert_eq!( //! // Evaluate the script, expects a 'bool' return //! engine.eval_file::("my_script.rhai".into())?, @@ -75,7 +76,7 @@ mod engine; mod error; mod fn_call; mod fn_func; -mod fn_native; +pub mod fn_native; mod fn_register; mod module; mod optimize; diff --git a/src/module.rs b/src/module.rs index 9e16e276..8b022dcb 100644 --- a/src/module.rs +++ b/src/module.rs @@ -2,12 +2,12 @@ use crate::any::{Dynamic, Variant}; use crate::calc_fn_hash; -use crate::engine::{make_getter, make_setter, Engine, FunctionsLib, FUNC_INDEXER}; -use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn}; +use crate::engine::{make_getter, make_setter, Engine, FUNC_INDEXER_GET, FUNC_INDEXER_SET}; +use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync}; use crate::parser::{ FnAccess, FnAccess::{Private, Public}, - AST, + ScriptFnDef, AST, }; use crate::result::EvalAltResult; use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; @@ -53,9 +53,6 @@ pub struct Module { StraightHasherBuilder, >, - /// Script-defined functions. - lib: FunctionsLib, - /// Iterator functions, keyed by the type producing the iterator. type_iterators: HashMap, @@ -68,10 +65,9 @@ impl fmt::Debug for Module { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "", + "", self.variables, self.functions.len(), - self.lib.len() ) } } @@ -186,6 +182,23 @@ impl Module { .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.to_string(), pos))) } + /// Set a script-defined function into the module. + /// + /// If there is an existing function of the same name and number of arguments, it is replaced. + pub(crate) fn set_script_fn(&mut self, fn_def: ScriptFnDef) { + // None + function name + number of arguments. + let hash_script = calc_fn_hash(empty(), &fn_def.name, fn_def.params.len(), empty()); + self.functions.insert( + hash_script, + ( + fn_def.name.to_string(), + fn_def.access, + Default::default(), + fn_def.into(), + ), + ); + } + /// Does a sub-module exist in the module? /// /// # Examples @@ -292,6 +305,29 @@ impl Module { hash_fn } + /// Set a Rust function taking a reference to the scripting `Engine`, plus a list of + /// mutable `Dynamic` references into the module, returning a hash key. + /// A list of `TypeId`'s is taken as the argument types. + /// + /// Use this to register a built-in function which must reference settings on the scripting + /// `Engine` (e.g. to prevent growing an array beyond the allowed maximum size). + /// + /// If there is a similar existing Rust function, it is replaced. + pub(crate) fn set_fn_var_args( + &mut self, + name: impl Into, + args: &[TypeId], + func: impl Fn(&Engine, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, + ) -> u64 { + let f = move |engine: &Engine, args: &mut FnCallArgs| func(engine, args).map(Dynamic::from); + self.set_fn( + name, + Public, + args, + CallableFunction::from_method(Box::new(f)), + ) + } + /// Set a Rust function taking no parameters into the module, returning a hash key. /// /// If there is a similar existing Rust function, it is replaced. @@ -308,10 +344,9 @@ impl Module { pub fn set_fn_0( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn() -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn() -> FuncReturn + Send + Sync + 'static, + func: impl Fn() -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |_: &mut FnCallArgs| func().map(Dynamic::from); + let f = move |_: &Engine, _: &mut FnCallArgs| func().map(Dynamic::from); let args = []; self.set_fn( name, @@ -337,11 +372,11 @@ impl Module { pub fn set_fn_1( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(A) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(A) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(A) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = - move |args: &mut FnCallArgs| func(mem::take(args[0]).cast::()).map(Dynamic::from); + let f = move |_: &Engine, args: &mut FnCallArgs| { + func(mem::take(args[0]).cast::()).map(Dynamic::from) + }; let args = [TypeId::of::()]; self.set_fn( name, @@ -367,10 +402,9 @@ impl Module { pub fn set_fn_1_mut( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(&mut A) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(&mut A) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(&mut A) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |args: &mut FnCallArgs| { + let f = move |_: &Engine, args: &mut FnCallArgs| { func(args[0].downcast_mut::().unwrap()).map(Dynamic::from) }; let args = [TypeId::of::()]; @@ -399,8 +433,7 @@ impl Module { pub fn set_getter_fn( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(&mut A) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(&mut A) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(&mut A) -> FuncReturn + SendSync + 'static, ) -> u64 { self.set_fn_1_mut(make_getter(&name.into()), func) } @@ -423,10 +456,9 @@ impl Module { pub fn set_fn_2( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(A, B) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(A, B) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(A, B) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |args: &mut FnCallArgs| { + let f = move |_: &Engine, args: &mut FnCallArgs| { let a = mem::take(args[0]).cast::(); let b = mem::take(args[1]).cast::(); @@ -460,10 +492,9 @@ impl Module { pub fn set_fn_2_mut( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(&mut A, B) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(&mut A, B) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |args: &mut FnCallArgs| { + let f = move |_: &Engine, args: &mut FnCallArgs| { let b = mem::take(args[1]).cast::(); let a = args[0].downcast_mut::().unwrap(); @@ -499,13 +530,12 @@ impl Module { pub fn set_setter_fn( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B) -> FuncReturn<()> + 'static, - #[cfg(feature = "sync")] func: impl Fn(&mut A, B) -> FuncReturn<()> + Send + Sync + 'static, + func: impl Fn(&mut A, B) -> FuncReturn<()> + SendSync + 'static, ) -> u64 { self.set_fn_2_mut(make_setter(&name.into()), func) } - /// Set a Rust indexer function taking two parameters (the first one mutable) into the module, + /// Set a Rust index getter taking two parameters (the first one mutable) into the module, /// returning a hash key. /// /// If there is a similar existing setter Rust function, it is replaced. @@ -516,19 +546,18 @@ impl Module { /// use rhai::{Module, ImmutableString}; /// /// let mut module = Module::new(); - /// let hash = module.set_indexer_fn(|x: &mut i64, y: ImmutableString| { + /// let hash = module.set_indexer_get_fn(|x: &mut i64, y: ImmutableString| { /// Ok(*x + y.len() as i64) /// }); /// assert!(module.get_fn(hash).is_some()); /// ``` #[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_index"))] - pub fn set_indexer_fn( + pub fn set_indexer_get_fn( &mut self, - #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(&mut A, B) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(&mut A, B) -> FuncReturn + SendSync + 'static, ) -> u64 { - self.set_fn_2_mut(FUNC_INDEXER, func) + self.set_fn_2_mut(FUNC_INDEXER_GET, func) } /// Set a Rust function taking three parameters into the module, returning a hash key. @@ -554,10 +583,9 @@ impl Module { >( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(A, B, C) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(A, B, C) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(A, B, C) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |args: &mut FnCallArgs| { + let f = move |_: &Engine, args: &mut FnCallArgs| { let a = mem::take(args[0]).cast::(); let b = mem::take(args[1]).cast::(); let c = mem::take(args[2]).cast::(); @@ -597,10 +625,9 @@ impl Module { >( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B, C) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(&mut A, B, C) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(&mut A, B, C) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |args: &mut FnCallArgs| { + let f = move |_: &Engine, args: &mut FnCallArgs| { let b = mem::take(args[1]).cast::(); let c = mem::take(args[2]).cast::(); let a = args[0].downcast_mut::().unwrap(); @@ -616,6 +643,43 @@ impl Module { ) } + /// Set a Rust index setter taking three parameters (the first one mutable) into the module, + /// returning a hash key. + /// + /// If there is a similar existing Rust function, it is replaced. + /// + /// # Examples + /// + /// ``` + /// use rhai::{Module, ImmutableString}; + /// + /// let mut module = Module::new(); + /// let hash = module.set_indexer_set_fn(|x: &mut i64, y: ImmutableString, value: i64| { + /// *x = y.len() as i64 + value; + /// Ok(()) + /// }); + /// assert!(module.get_fn(hash).is_some()); + /// ``` + pub fn set_indexer_set_fn( + &mut self, + func: impl Fn(&mut A, B, A) -> FuncReturn<()> + SendSync + 'static, + ) -> u64 { + let f = move |_: &Engine, args: &mut FnCallArgs| { + let b = mem::take(args[1]).cast::(); + let c = mem::take(args[2]).cast::(); + let a = args[0].downcast_mut::().unwrap(); + + func(a, b, c).map(Dynamic::from) + }; + let args = [TypeId::of::(), TypeId::of::(), TypeId::of::()]; + self.set_fn( + FUNC_INDEXER_SET, + Public, + &args, + CallableFunction::from_method(Box::new(f)), + ) + } + /// Set a Rust function taking four parameters into the module, returning a hash key. /// /// If there is a similar existing Rust function, it is replaced. @@ -640,10 +704,9 @@ impl Module { >( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(A, B, C, D) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(A, B, C, D) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(A, B, C, D) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |args: &mut FnCallArgs| { + let f = move |_: &Engine, args: &mut FnCallArgs| { let a = mem::take(args[0]).cast::(); let b = mem::take(args[1]).cast::(); let c = mem::take(args[2]).cast::(); @@ -690,10 +753,9 @@ impl Module { >( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B, C, D) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(&mut A, B, C, D) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(&mut A, B, C, D) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |args: &mut FnCallArgs| { + let f = move |_: &Engine, args: &mut FnCallArgs| { let b = mem::take(args[1]).cast::(); let c = mem::take(args[2]).cast::(); let d = mem::take(args[3]).cast::(); @@ -740,9 +802,9 @@ impl Module { pub(crate) fn get_qualified_fn( &mut self, name: &str, - hash_fn_native: u64, + hash_qualified_fn: u64, ) -> Result<&CallableFunction, Box> { - self.all_functions.get(&hash_fn_native).ok_or_else(|| { + self.all_functions.get(&hash_qualified_fn).ok_or_else(|| { Box::new(EvalAltResult::ErrorFunctionNotFound( name.to_string(), Position::none(), @@ -750,6 +812,41 @@ impl Module { }) } + /// Merge another module into this module. + pub fn merge(&mut self, other: &Self) { + self.variables + .extend(other.variables.iter().map(|(k, v)| (k.clone(), v.clone()))); + self.functions + .extend(other.functions.iter().map(|(&k, v)| (k, v.clone()))); + self.type_iterators + .extend(other.type_iterators.iter().map(|(&k, v)| (k, v.clone()))); + } + + /// Get the number of variables in the module. + pub fn num_var(&self) -> usize { + self.variables.len() + } + /// Get the number of functions in the module. + pub fn num_fn(&self) -> usize { + self.variables.len() + } + /// Get the number of type iterators in the module. + pub fn num_iter(&self) -> usize { + self.variables.len() + } + + /// Get an iterator to the variables in the module. + pub fn iter_var(&self) -> impl Iterator { + self.variables.iter() + } + + /// Get an iterator to the functions in the module. + pub(crate) fn iter_fn( + &self, + ) -> impl Iterator, CallableFunction)> { + self.functions.values() + } + /// Create a new `Module` by evaluating an `AST`. /// /// # Examples @@ -795,12 +892,12 @@ impl Module { }, ); - module.lib = module.lib.merge(ast.lib()); + module.merge(ast.lib()); Ok(module) } - /// Scan through all the sub-modules in the `Module` build an index of all + /// Scan through all the sub-modules in the module build an index of all /// variables and external Rust functions via hashing. pub(crate) fn index_all_sub_modules(&mut self) { // Collect a particular module. @@ -830,34 +927,31 @@ impl Module { Private => continue, Public => (), } - // Rust functions are indexed in two steps: - // 1) Calculate a hash in a similar manner to script-defined functions, - // i.e. qualifiers + function name + number of arguments. - let hash_fn_def = - calc_fn_hash(qualifiers.iter().map(|&v| v), name, params.len(), empty()); - // 2) Calculate a second hash with no qualifiers, empty function name, - // zero number of arguments, and the actual list of argument `TypeId`'.s - let hash_fn_args = calc_fn_hash(empty(), "", 0, params.iter().cloned()); - // 3) The final hash is the XOR of the two hashes. - let hash_fn_native = hash_fn_def ^ hash_fn_args; - functions.push((hash_fn_native, func.clone())); - } - // Index all script-defined functions - for fn_def in module.lib.values() { - match fn_def.access { - // Private functions are not exported - Private => continue, - Public => (), + if func.is_script() { + let fn_def = func.get_shared_fn_def(); + // Qualifiers + function name + number of arguments. + let hash_qualified_script = calc_fn_hash( + qualifiers.iter().map(|&v| v), + &fn_def.name, + fn_def.params.len(), + empty(), + ); + functions.push((hash_qualified_script, fn_def.into())); + } else { + // Qualified Rust functions are indexed in two steps: + // 1) Calculate a hash in a similar manner to script-defined functions, + // i.e. qualifiers + function name + number of arguments. + let hash_qualified_script = + calc_fn_hash(qualifiers.iter().map(|&v| v), name, params.len(), empty()); + // 2) Calculate a second hash with no qualifiers, empty function name, + // zero number of arguments, and the actual list of argument `TypeId`'.s + let hash_fn_args = calc_fn_hash(empty(), "", 0, params.iter().cloned()); + // 3) The final hash is the XOR of the two hashes. + let hash_qualified_fn = hash_qualified_script ^ hash_fn_args; + + functions.push((hash_qualified_fn, func.clone())); } - // Qualifiers + function name + number of arguments. - let hash_fn_def = calc_fn_hash( - qualifiers.iter().map(|&v| v), - &fn_def.name, - fn_def.params.len(), - empty(), - ); - functions.push((hash_fn_def, CallableFunction::Script(fn_def.clone()).into())); } } @@ -945,27 +1039,11 @@ impl ModuleRef { } /// Trait that encapsulates a module resolution service. -#[cfg(not(feature = "no_module"))] -#[cfg(not(feature = "sync"))] -pub trait ModuleResolver { +pub trait ModuleResolver: SendSync { /// Resolve a module based on a path string. fn resolve( &self, - engine: &Engine, - scope: Scope, - path: &str, - pos: Position, - ) -> Result>; -} - -/// Trait that encapsulates a module resolution service. -#[cfg(not(feature = "no_module"))] -#[cfg(feature = "sync")] -pub trait ModuleResolver: Send + Sync { - /// Resolve a module based on a path string. - fn resolve( - &self, - engine: &Engine, + _: &Engine, scope: Scope, path: &str, pos: Position, @@ -976,13 +1054,17 @@ pub trait ModuleResolver: Send + Sync { #[cfg(not(feature = "no_module"))] pub mod resolvers { #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] pub use super::file::FileModuleResolver; pub use super::stat::StaticModuleResolver; } +#[cfg(feature = "no_module")] +pub mod resolvers {} /// Script file-based module resolver. #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] mod file { use super::*; use crate::stdlib::path::PathBuf; diff --git a/src/optimize.rs b/src/optimize.rs index 78613ace..7895fc68 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -1,13 +1,9 @@ use crate::any::Dynamic; use crate::calc_fn_hash; -use crate::engine::{ - Engine, FunctionsLib, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_PRINT, - KEYWORD_TYPE_OF, -}; -use crate::parser::{map_dynamic_to_expr, Expr, FnDef, ReturnType, Stmt, AST}; -use crate::result::EvalAltResult; +use crate::engine::{Engine, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_PRINT, KEYWORD_TYPE_OF}; +use crate::module::Module; +use crate::parser::{map_dynamic_to_expr, Expr, ReturnType, ScriptFnDef, Stmt, AST}; use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; -use crate::token::Position; use crate::utils::StaticVec; use crate::stdlib::{ @@ -56,14 +52,14 @@ struct State<'a> { /// An `Engine` instance for eager function evaluation. engine: &'a Engine, /// Library of script-defined functions. - lib: &'a FunctionsLib, + lib: &'a Module, /// Optimization level. optimization_level: OptimizationLevel, } impl<'a> State<'a> { /// Create a new State. - pub fn new(engine: &'a Engine, lib: &'a FunctionsLib, level: OptimizationLevel) -> Self { + pub fn new(engine: &'a Engine, lib: &'a Module, level: OptimizationLevel) -> Self { Self { changed: false, constants: vec![], @@ -113,8 +109,7 @@ fn call_fn_with_constant_arguments( state: &State, fn_name: &str, arg_values: &mut [Dynamic], - pos: Position, -) -> Result, Box> { +) -> Option { // Search built-in's and external functions let hash_fn = calc_fn_hash( empty(), @@ -134,11 +129,10 @@ fn call_fn_with_constant_arguments( arg_values.iter_mut().collect::>().as_mut(), false, None, - pos, 0, ) .map(|(v, _)| Some(v)) - .or_else(|_| Ok(None)) + .unwrap_or_else(|_| None) } /// Optimize a statement. @@ -551,14 +545,15 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { && state.optimization_level == OptimizationLevel::Full // full optimizations && x.3.iter().all(|expr| expr.is_constant()) // all arguments are constants => { - let ((name, native_only, pos), _, _, args, def_value) = x.as_mut(); + let ((name, _, pos), _, _, args, def_value) = x.as_mut(); - // First search in script-defined functions (can override built-in) + // First search in functions lib (can override built-in) // Cater for both normal function call style and method call style (one additional arguments) - if !*native_only && state.lib.values().find(|f| - &f.name == name - && (args.len()..=args.len() + 1).contains(&f.params.len()) - ).is_some() { + if state.lib.iter_fn().find(|(_, _, _, f)| { + if !f.is_script() { return false; } + let fn_def = f.get_fn_def(); + &fn_def.name == name && (args.len()..=args.len() + 1).contains(&fn_def.params.len()) + }).is_some() { // A script-defined function overrides the built-in function - do not make the call x.3 = x.3.into_iter().map(|a| optimize_expr(a, state)).collect(); return Expr::FnCall(x); @@ -574,22 +569,22 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { "" }; - call_fn_with_constant_arguments(&state, name, arg_values.as_mut(), *pos).ok() - .and_then(|result| - result.or_else(|| { - if !arg_for_type_of.is_empty() { - // Handle `type_of()` - Some(arg_for_type_of.to_string().into()) - } else { - // Otherwise use the default value, if any - def_value.clone() - } - }).and_then(|result| map_dynamic_to_expr(result, *pos)) - .map(|expr| { - state.set_dirty(); - expr - }) - ).unwrap_or_else(|| { + call_fn_with_constant_arguments(&state, name, arg_values.as_mut()) + .or_else(|| { + if !arg_for_type_of.is_empty() { + // Handle `type_of()` + Some(arg_for_type_of.to_string().into()) + } else { + // Otherwise use the default value, if any + def_value.clone() + } + }) + .and_then(|result| map_dynamic_to_expr(result, *pos)) + .map(|expr| { + state.set_dirty(); + expr + }) + .unwrap_or_else(|| { // Optimize function call arguments x.3 = x.3.into_iter().map(|a| optimize_expr(a, state)).collect(); Expr::FnCall(x) @@ -620,7 +615,7 @@ fn optimize( statements: Vec, engine: &Engine, scope: &Scope, - lib: &FunctionsLib, + lib: &Module, level: OptimizationLevel, ) -> Vec { // If optimization level is None then skip optimizing @@ -707,7 +702,7 @@ pub fn optimize_into_ast( engine: &Engine, scope: &Scope, statements: Vec, - functions: Vec, + functions: Vec, level: OptimizationLevel, ) -> AST { #[cfg(feature = "no_optimize")] @@ -715,37 +710,65 @@ pub fn optimize_into_ast( #[cfg(not(feature = "no_function"))] let lib = { + let mut module = Module::new(); + if !level.is_none() { - let lib = FunctionsLib::from_iter(functions.iter().cloned()); + // We only need the script library's signatures for optimization purposes + let mut lib2 = Module::new(); - FunctionsLib::from_iter(functions.into_iter().map(|mut fn_def| { - let pos = fn_def.body.position(); - - // Optimize the function body - let mut body = optimize(vec![fn_def.body], engine, &Scope::new(), &lib, level); - - // {} -> Noop - fn_def.body = match body.pop().unwrap_or_else(|| Stmt::Noop(pos)) { - // { return val; } -> val - Stmt::ReturnWithVal(x) if x.1.is_some() && (x.0).0 == ReturnType::Return => { - Stmt::Expr(Box::new(x.1.unwrap())) + functions + .iter() + .map(|fn_def| { + ScriptFnDef { + name: fn_def.name.clone(), + access: fn_def.access, + body: Default::default(), + params: fn_def.params.clone(), + pos: fn_def.pos, } - // { return; } -> () - Stmt::ReturnWithVal(x) if x.1.is_none() && (x.0).0 == ReturnType::Return => { - Stmt::Expr(Box::new(Expr::Unit((x.0).1))) - } - // All others - stmt => stmt, - }; - fn_def - })) + .into() + }) + .for_each(|fn_def| lib2.set_script_fn(fn_def)); + + functions + .into_iter() + .map(|mut fn_def| { + let pos = fn_def.body.position(); + + // Optimize the function body + let mut body = optimize(vec![fn_def.body], engine, &Scope::new(), &lib2, level); + + // {} -> Noop + fn_def.body = match body.pop().unwrap_or_else(|| Stmt::Noop(pos)) { + // { return val; } -> val + Stmt::ReturnWithVal(x) + if x.1.is_some() && (x.0).0 == ReturnType::Return => + { + Stmt::Expr(Box::new(x.1.unwrap())) + } + // { return; } -> () + Stmt::ReturnWithVal(x) + if x.1.is_none() && (x.0).0 == ReturnType::Return => + { + Stmt::Expr(Box::new(Expr::Unit((x.0).1))) + } + // All others + stmt => stmt, + }; + fn_def.into() + }) + .for_each(|fn_def| module.set_script_fn(fn_def)); } else { - FunctionsLib::from_iter(functions.into_iter()) + functions + .into_iter() + .for_each(|fn_def| module.set_script_fn(fn_def)); } + + module }; #[cfg(feature = "no_function")] - let lib: FunctionsLib = Default::default(); + let lib = Default::default(); AST::new( match level { diff --git a/src/packages/arithmetic.rs b/src/packages/arithmetic.rs index ead54f8c..cf109b0c 100644 --- a/src/packages/arithmetic.rs +++ b/src/packages/arithmetic.rs @@ -275,27 +275,51 @@ def_package!(crate:ArithmeticPackage:"Basic arithmetic", lib, { #[cfg(not(feature = "unchecked"))] { // Checked basic arithmetic - reg_op!(lib, "+", add, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "-", sub, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "*", mul, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "/", div, i8, u8, i16, u16, i32, u32, u64, i128, u128); + reg_op!(lib, "+", add, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "-", sub, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "*", mul, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "/", div, i8, u8, i16, u16, i32, u32, u64); // Checked bit shifts - reg_op!(lib, "<<", shl, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, ">>", shr, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "%", modulo, i8, u8, i16, u16, i32, u32, u64, i128, u128); + reg_op!(lib, "<<", shl, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, ">>", shr, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "%", modulo, i8, u8, i16, u16, i32, u32, u64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_op!(lib, "+", add, i128, u128); + reg_op!(lib, "-", sub, i128, u128); + reg_op!(lib, "*", mul, i128, u128); + reg_op!(lib, "/", div, i128, u128); + // Checked bit shifts + reg_op!(lib, "<<", shl, i128, u128); + reg_op!(lib, ">>", shr, i128, u128); + reg_op!(lib, "%", modulo, i128, u128); + } } #[cfg(feature = "unchecked")] { // Unchecked basic arithmetic - reg_op!(lib, "+", add_u, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "-", sub_u, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "*", mul_u, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "/", div_u, i8, u8, i16, u16, i32, u32, u64, i128, u128); + reg_op!(lib, "+", add_u, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "-", sub_u, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "*", mul_u, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "/", div_u, i8, u8, i16, u16, i32, u32, u64); // Unchecked bit shifts - reg_op!(lib, "<<", shl_u, i64, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, ">>", shr_u, i64, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "%", modulo_u, i8, u8, i16, u16, i32, u32, u64, i128, u128); + reg_op!(lib, "<<", shl_u, i64, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, ">>", shr_u, i64, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "%", modulo_u, i8, u8, i16, u16, i32, u32, u64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_op!(lib, "+", add_u, i128, u128); + reg_op!(lib, "-", sub_u, i128, u128); + reg_op!(lib, "*", mul_u, i128, u128); + reg_op!(lib, "/", div_u, i128, u128); + // Unchecked bit shifts + reg_op!(lib, "<<", shl_u, i128, u128); + reg_op!(lib, ">>", shr_u, i128, u128); + reg_op!(lib, "%", modulo_u, i128, u128); + } } } @@ -311,9 +335,16 @@ def_package!(crate:ArithmeticPackage:"Basic arithmetic", lib, { #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] { - reg_op!(lib, "|", binary_or, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "&", binary_and, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "^", binary_xor, i8, u8, i16, u16, i32, u32, u64, i128, u128); + reg_op!(lib, "|", binary_or, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "&", binary_and, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "^", binary_xor, i8, u8, i16, u16, i32, u32, u64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_op!(lib, "|", binary_or, i128, u128); + reg_op!(lib, "&", binary_and, i128, u128); + reg_op!(lib, "^", binary_xor, i128, u128); + } } #[cfg(not(feature = "no_float"))] @@ -343,8 +374,14 @@ def_package!(crate:ArithmeticPackage:"Basic arithmetic", lib, { #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] { - reg_unary!(lib, "-", neg, i8, i16, i32, i64, i128); - reg_unary!(lib, "abs", abs, i8, i16, i32, i64, i128); + reg_unary!(lib, "-", neg, i8, i16, i32, i64); + reg_unary!(lib, "abs", abs, i8, i16, i32, i64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_unary!(lib, "-", neg, i128); + reg_unary!(lib, "abs", abs, i128); + } } } @@ -357,8 +394,14 @@ def_package!(crate:ArithmeticPackage:"Basic arithmetic", lib, { #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] { - reg_unary!(lib, "-", neg_u, i8, i16, i32, i64, i128); - reg_unary!(lib, "abs", abs_u, i8, i16, i32, i64, i128); + reg_unary!(lib, "-", neg_u, i8, i16, i32, i64); + reg_unary!(lib, "abs", abs_u, i8, i16, i32, i64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_unary!(lib, "-", neg_u, i128); + reg_unary!(lib, "abs", abs_u, i128); + } } } }); diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index 19206e36..59358063 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -2,9 +2,11 @@ use crate::any::{Dynamic, Variant}; use crate::def_package; -use crate::engine::Array; +use crate::engine::{Array, Engine}; use crate::module::FuncReturn; use crate::parser::{ImmutableString, INT}; +use crate::result::EvalAltResult; +use crate::token::Position; use crate::stdlib::{any::TypeId, boxed::Box}; @@ -23,8 +25,26 @@ fn ins(list: &mut Array, position: INT, item: T) -> FuncRetu } Ok(()) } -fn pad(list: &mut Array, len: INT, item: T) -> FuncReturn<()> { +fn pad(engine: &Engine, args: &mut [&mut Dynamic]) -> FuncReturn<()> { + let len = *args[1].downcast_ref::().unwrap(); + + // Check if array will be over max size limit + #[cfg(not(feature = "unchecked"))] + { + if engine.max_array_size > 0 && len > 0 && (len as usize) > engine.max_array_size { + return Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Size of array".to_string(), + engine.max_array_size, + len as usize, + Position::none(), + ))); + } + } + if len >= 0 { + let item = args[2].downcast_ref::().unwrap().clone(); + let list = args[0].downcast_mut::().unwrap(); + while list.len() < len as usize { push(list, item.clone())?; } @@ -42,11 +62,21 @@ macro_rules! reg_tri { $( $lib.set_fn_3_mut($op, $func::<$par>); )* }; } +macro_rules! reg_pad { + ($lib:expr, $op:expr, $func:ident, $($par:ty),*) => { + $({ + $lib.set_fn_var_args($op, + &[TypeId::of::(), TypeId::of::(), TypeId::of::<$par>()], + $func::<$par> + ); + })* + }; +} #[cfg(not(feature = "no_index"))] def_package!(crate:BasicArrayPackage:"Basic array utilities.", lib, { reg_op!(lib, "push", push, INT, bool, char, ImmutableString, Array, ()); - reg_tri!(lib, "pad", pad, INT, bool, char, ImmutableString, Array, ()); + reg_pad!(lib, "pad", pad, INT, bool, char, ImmutableString, Array, ()); reg_tri!(lib, "insert", ins, INT, bool, char, ImmutableString, Array, ()); lib.set_fn_2_mut("append", |x: &mut Array, y: Array| { @@ -68,15 +98,22 @@ def_package!(crate:BasicArrayPackage:"Basic array utilities.", lib, { #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] { - reg_op!(lib, "push", push, i8, u8, i16, u16, i32, i64, u32, u64, i128, u128); - reg_tri!(lib, "pad", pad, i8, u8, i16, u16, i32, u32, i64, u64, i128, u128); - reg_tri!(lib, "insert", ins, i8, u8, i16, u16, i32, i64, u32, u64, i128, u128); + reg_op!(lib, "push", push, i8, u8, i16, u16, i32, i64, u32, u64); + reg_pad!(lib, "pad", pad, i8, u8, i16, u16, i32, u32, i64, u64); + reg_tri!(lib, "insert", ins, i8, u8, i16, u16, i32, i64, u32, u64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_op!(lib, "push", push, i128, u128); + reg_pad!(lib, "pad", pad, i128, u128); + reg_tri!(lib, "insert", ins, i128, u128); + } } #[cfg(not(feature = "no_float"))] { reg_op!(lib, "push", push, f32, f64); - reg_tri!(lib, "pad", pad, f32, f64); + reg_pad!(lib, "pad", pad, f32, f64); reg_tri!(lib, "insert", ins, f32, f64); } diff --git a/src/packages/iter_basic.rs b/src/packages/iter_basic.rs index ebf55992..cccf4b6c 100644 --- a/src/packages/iter_basic.rs +++ b/src/packages/iter_basic.rs @@ -85,7 +85,10 @@ def_package!(crate:BasicIteratorPackage:"Basic range iterators.", lib, { ) } - reg_range!(lib, "range", i8, u8, i16, u16, i32, i64, u32, u64, i128, u128); + reg_range!(lib, "range", i8, u8, i16, u16, i32, i64, u32, u64); + + #[cfg(not(target_arch = "wasm32"))] + reg_range!(lib, "range", i128, u128); } reg_step::(lib); @@ -103,6 +106,9 @@ def_package!(crate:BasicIteratorPackage:"Basic range iterators.", lib, { ) } - reg_step!(lib, "range", i8, u8, i16, u16, i32, i64, u32, u64, i128, u128); + reg_step!(lib, "range", i8, u8, i16, u16, i32, i64, u32, u64); + + #[cfg(not(target_arch = "wasm32"))] + reg_step!(lib, "range", i128, u128); } }); diff --git a/src/packages/logic.rs b/src/packages/logic.rs index 41ca035f..1cb4b437 100644 --- a/src/packages/logic.rs +++ b/src/packages/logic.rs @@ -36,12 +36,22 @@ def_package!(crate:LogicPackage:"Logical operators.", lib, { #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] { - reg_op!(lib, "<", lt, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "<=", lte, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, ">", gt, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, ">=", gte, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "==", eq, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "!=", ne, i8, u8, i16, u16, i32, u32, u64, i128, u128); + reg_op!(lib, "<", lt, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "<=", lte, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, ">", gt, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, ">=", gte, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "==", eq, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "!=", ne, i8, u8, i16, u16, i32, u32, u64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_op!(lib, "<", lt, i128, u128); + reg_op!(lib, "<=", lte, i128, u128); + reg_op!(lib, ">", gt, i128, u128); + reg_op!(lib, ">=", gte, i128, u128); + reg_op!(lib, "==", eq, i128, u128); + reg_op!(lib, "!=", ne, i128, u128); + } } #[cfg(not(feature = "no_float"))] diff --git a/src/packages/math_basic.rs b/src/packages/math_basic.rs index eea49727..e7ce43b7 100644 --- a/src/packages/math_basic.rs +++ b/src/packages/math_basic.rs @@ -70,8 +70,12 @@ def_package!(crate:BasicMathPackage:"Basic mathematic functions.", lib, { lib.set_fn_1("to_float", |x: u32| Ok(x as FLOAT)); lib.set_fn_1("to_float", |x: i64| Ok(x as FLOAT)); lib.set_fn_1("to_float", |x: u64| Ok(x as FLOAT)); - lib.set_fn_1("to_float", |x: i128| Ok(x as FLOAT)); - lib.set_fn_1("to_float", |x: u128| Ok(x as FLOAT)); + + #[cfg(not(target_arch = "wasm32"))] + { + lib.set_fn_1("to_float", |x: i128| Ok(x as FLOAT)); + lib.set_fn_1("to_float", |x: u128| Ok(x as FLOAT)); + } } } diff --git a/src/packages/string_basic.rs b/src/packages/string_basic.rs index 262a4f3a..338cf5ec 100644 --- a/src/packages/string_basic.rs +++ b/src/packages/string_basic.rs @@ -52,9 +52,16 @@ def_package!(crate:BasicStringPackage:"Basic string utilities, including printin reg_op!(lib, KEYWORD_PRINT, to_string, i8, u8, i16, u16, i32, u32); reg_op!(lib, FUNC_TO_STRING, to_string, i8, u8, i16, u16, i32, u32); reg_op!(lib, KEYWORD_DEBUG, to_debug, i8, u8, i16, u16, i32, u32); - reg_op!(lib, KEYWORD_PRINT, to_string, i64, u64, i128, u128); - reg_op!(lib, FUNC_TO_STRING, to_string, i64, u64, i128, u128); - reg_op!(lib, KEYWORD_DEBUG, to_debug, i64, u64, i128, u128); + reg_op!(lib, KEYWORD_PRINT, to_string, i64, u64); + reg_op!(lib, FUNC_TO_STRING, to_string, i64, u64); + reg_op!(lib, KEYWORD_DEBUG, to_debug, i64, u64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_op!(lib, KEYWORD_PRINT, to_string, i128, u128); + reg_op!(lib, FUNC_TO_STRING, to_string, i128, u128); + reg_op!(lib, KEYWORD_DEBUG, to_debug, i128, u128); + } } #[cfg(not(feature = "no_float"))] diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index e86f5fb1..46805750 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -1,15 +1,21 @@ +use crate::any::Dynamic; use crate::def_package; +use crate::engine::Engine; use crate::module::FuncReturn; use crate::parser::{ImmutableString, INT}; +use crate::result::EvalAltResult; +use crate::token::Position; use crate::utils::StaticVec; #[cfg(not(feature = "no_index"))] use crate::engine::Array; use crate::stdlib::{ + any::TypeId, fmt::Display, format, string::{String, ToString}, + vec::Vec, }; fn prepend(x: T, y: ImmutableString) -> FuncReturn { @@ -89,8 +95,14 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] { - reg_op!(lib, "+", append, i8, u8, i16, u16, i32, i64, u32, u64, i128, u128); - reg_op!(lib, "+", prepend, i8, u8, i16, u16, i32, i64, u32, u64, i128, u128); + reg_op!(lib, "+", append, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op!(lib, "+", prepend, i8, u8, i16, u16, i32, i64, u32, u64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_op!(lib, "+", append, i128, u128); + reg_op!(lib, "+", prepend, i128, u128); + } } #[cfg(not(feature = "no_float"))] @@ -210,14 +222,43 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str Ok(()) }, ); - lib.set_fn_3_mut( + lib.set_fn_var_args( "pad", - |s: &mut ImmutableString, len: INT, ch: char| { + &[TypeId::of::(), TypeId::of::(), TypeId::of::()], + |engine: &Engine, args: &mut [&mut Dynamic]| { + let len = *args[1].downcast_ref::< INT>().unwrap(); + + // Check if string will be over max size limit + #[cfg(not(feature = "unchecked"))] + { + if engine.max_string_size > 0 && len > 0 && (len as usize) > engine.max_string_size { + return Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Length of string".to_string(), + engine.max_string_size, + len as usize, + Position::none(), + ))); + } + } + + let ch = *args[2].downcast_ref::< char>().unwrap(); + let s = args[0].downcast_mut::().unwrap(); + let copy = s.make_mut(); for _ in 0..copy.chars().count() - len as usize { copy.push(ch); } - Ok(()) + + if engine.max_string_size > 0 && copy.len() > engine.max_string_size { + Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Length of string".to_string(), + engine.max_string_size, + copy.len(), + Position::none(), + ))) + } else { + Ok(()) + } }, ); lib.set_fn_3_mut( @@ -259,4 +300,12 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str Ok(()) }, ); + + // Register string iterator + lib.set_iter( + TypeId::of::(), + |arr| Box::new( + arr.cast::().chars().collect::>().into_iter().map(Into::into) + ) as Box>, + ); }); diff --git a/src/packages/time_basic.rs b/src/packages/time_basic.rs index b8e13604..9fd4e21f 100644 --- a/src/packages/time_basic.rs +++ b/src/packages/time_basic.rs @@ -1,3 +1,4 @@ +#![cfg(not(feature = "no_std"))] use super::logic::{eq, gt, gte, lt, lte, ne}; use super::math_basic::MAX_INT; @@ -7,13 +8,15 @@ use crate::parser::INT; use crate::result::EvalAltResult; use crate::token::Position; -#[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] use crate::stdlib::time::Instant; +#[cfg(target_arch = "wasm32")] +use instant::Instant; + #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; -#[cfg(not(feature = "no_std"))] def_package!(crate:BasicTimePackage:"Basic timing utilities.", lib, { // Register date/time functions lib.set_fn_0("timestamp", || Ok(Instant::now())); diff --git a/src/parser.rs b/src/parser.rs index 2a20422e..5d0bd902 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,27 +2,21 @@ use crate::any::{Dynamic, Union}; use crate::calc_fn_hash; -use crate::engine::{make_getter, make_setter, Engine, FunctionsLib}; +use crate::engine::{make_getter, make_setter, Engine}; 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, TokenIterator}; +use crate::token::{Position, Token, TokenStream}; use crate::utils::{StaticVec, StraightHasherBuilder}; -#[cfg(not(feature = "no_module"))] -use crate::module::ModuleRef; - -#[cfg(feature = "no_module")] -#[derive(Debug, Eq, PartialEq, Clone, Hash, Copy, Default)] -pub struct ModuleRef; - use crate::stdlib::{ borrow::Cow, boxed::Box, char, collections::HashMap, format, - iter::{empty, Peekable}, + iter::empty, mem, num::NonZeroUsize, ops::{Add, Deref, DerefMut}, @@ -61,12 +55,12 @@ pub struct AST( /// Global statements. Vec, /// Script-defined functions. - FunctionsLib, + Module, ); impl AST { /// Create a new `AST`. - pub fn new(statements: Vec, lib: FunctionsLib) -> Self { + pub fn new(statements: Vec, lib: Module) -> Self { Self(statements, lib) } @@ -81,7 +75,7 @@ impl AST { } /// Get the script-defined functions. - pub(crate) fn lib(&self) -> &FunctionsLib { + pub(crate) fn lib(&self) -> &Module { &self.1 } @@ -141,7 +135,10 @@ impl AST { (true, true) => vec![], }; - Self::new(ast, functions.merge(&other.1)) + let mut functions = functions.clone(); + functions.merge(&other.1); + + Self::new(ast, functions) } /// Clear all function definitions in the `AST`. @@ -176,7 +173,7 @@ pub enum FnAccess { /// A scripted function definition. #[derive(Debug, Clone)] -pub struct FnDef { +pub struct ScriptFnDef { /// Function name. pub name: String, /// Function access mode. @@ -198,18 +195,33 @@ pub enum ReturnType { Exception, } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] struct ParseState { /// Encapsulates a local stack with variable names to simulate an actual runtime scope. stack: Vec<(String, ScopeEntryType)>, + /// Maximum levels of expression nesting. max_expr_depth: usize, + /// Maximum length of a string. + pub max_string_size: usize, + /// Maximum length of an array. + pub max_array_size: usize, + /// Maximum number of properties in a map. + pub max_map_size: usize, } impl ParseState { /// Create a new `ParseState`. - pub fn new(max_expr_depth: usize) -> Self { + pub fn new( + max_expr_depth: usize, + max_string_size: usize, + max_array_size: usize, + max_map_size: usize, + ) -> Self { Self { max_expr_depth, + max_string_size, + max_array_size, + max_map_size, ..Default::default() } } @@ -259,6 +271,43 @@ impl DerefMut for ParseState { } } +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +/// A type that encapsulates all the settings for a particular parsing function. +struct ParseSettings { + /// Current position. + pos: Position, + /// Is the construct being parsed located at global level? + is_global: bool, + /// Is the current position inside a loop? + is_breakable: bool, + /// Is if-expression allowed? + allow_if_expr: bool, + /// Is statement-expression allowed? + allow_stmt_expr: bool, + /// Current expression nesting level. + level: usize, +} + +impl ParseSettings { + /// Create a new `ParseSettings` with one higher expression level. + pub fn level_up(&self) -> Self { + Self { + level: self.level + 1, + ..*self + } + } + /// Make sure that the current level of expression nesting is within the maximum limit. + pub fn ensure_level_within_max_limit(&self, limit: usize) -> Result<(), ParseError> { + if limit == 0 { + Ok(()) + } else if self.level > limit { + Err(PERR::ExprTooDeep.into_err(self.pos)) + } else { + Ok(()) + } + } +} + /// A statement. /// /// Each variant is at most one pointer in size (for speed), @@ -469,7 +518,7 @@ impl Expr { ))) } - _ => panic!("cannot get value of non-constant expression"), + _ => unreachable!("cannot get value of non-constant expression"), } } @@ -494,7 +543,7 @@ impl Expr { Self::Array(x) if x.0.iter().all(Self::is_constant) => "array".to_string(), - _ => panic!("cannot get value of non-constant expression"), + _ => unreachable!("cannot get value of non-constant expression"), } } @@ -577,7 +626,7 @@ impl Expr { Self::Variable(_) => true, - expr => expr.is_constant(), + _ => self.is_constant(), } } @@ -628,29 +677,33 @@ impl Expr { | Self::Or(_) | Self::True(_) | Self::False(_) - | Self::Unit(_) => false, + | Self::Unit(_) + | Self::Assignment(_) => false, Self::StringConstant(_) | Self::Stmt(_) | Self::FnCall(_) - | Self::Assignment(_) | Self::Dot(_) | Self::Index(_) | Self::Array(_) | Self::Map(_) => match token { + #[cfg(not(feature = "no_index"))] Token::LeftBracket => true, _ => false, }, Self::Variable(_) => match token { - Token::LeftBracket | Token::LeftParen => true, - #[cfg(not(feature = "no_module"))] + #[cfg(not(feature = "no_index"))] + Token::LeftBracket => true, + Token::LeftParen => true, Token::DoubleColon => true, _ => false, }, Self::Property(_) => match token { - Token::LeftBracket | Token::LeftParen => true, + #[cfg(not(feature = "no_index"))] + Token::LeftBracket => true, + Token::LeftParen => true, _ => false, }, } @@ -671,11 +724,11 @@ impl Expr { } /// Consume a particular token, checking that it is the expected one. -fn eat_token(input: &mut Peekable, token: Token) -> Position { +fn eat_token(input: &mut TokenStream, token: Token) -> Position { let (t, pos) = input.next().unwrap(); if t != token { - panic!( + unreachable!( "expecting {} (found {}) at {}", token.syntax(), t.syntax(), @@ -686,7 +739,7 @@ fn eat_token(input: &mut Peekable, token: Token) -> Position { } /// Match a particular token, consuming it if matched. -fn match_token(input: &mut Peekable, token: Token) -> Result { +fn match_token(input: &mut TokenStream, token: Token) -> Result { let (t, _) = input.peek().unwrap(); if *t == token { eat_token(input, token); @@ -697,28 +750,24 @@ fn match_token(input: &mut Peekable, token: Token) -> Result( - input: &mut Peekable>, +fn parse_paren_expr( + input: &mut TokenStream, state: &mut ParseState, - pos: Position, - level: usize, - allow_stmt_expr: bool, + settings: ParseSettings, ) -> Result { - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.ensure_level_within_max_limit(state.max_expr_depth)?; if match_token(input, Token::RightParen)? { - return Ok(Expr::Unit(pos)); + return Ok(Expr::Unit(settings.pos)); } - let expr = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let expr = parse_expr(input, state, settings.level_up())?; match input.next().unwrap() { // ( xxx ) (Token::RightParen, _) => Ok(expr), // ( - (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), // ( xxx ??? (_, pos) => Err(PERR::MissingToken( Token::RightParen.into(), @@ -729,20 +778,15 @@ fn parse_paren_expr<'a>( } /// Parse a function call. -fn parse_call_expr<'a>( - input: &mut Peekable>, +fn parse_call_expr( + input: &mut TokenStream, state: &mut ParseState, id: String, mut modules: Option>, - begin: Position, - level: usize, - allow_stmt_expr: bool, + settings: ParseSettings, ) -> Result { - let (token, pos) = input.peek().unwrap(); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(*pos)); - } + let (token, _) = input.peek().unwrap(); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let mut args = StaticVec::new(); @@ -753,17 +797,57 @@ fn parse_call_expr<'a>( Token::RightParen.into(), format!("to close the arguments list of this function call '{}'", id), ) - .into_err(*pos)) + .into_err(settings.pos)) } // id - Token::LexError(err) => return Err(PERR::BadInput(err.to_string()).into_err(*pos)), + Token::LexError(err) => return Err(err.into_err(settings.pos)), // id() Token::RightParen => { eat_token(input, Token::RightParen); - #[cfg(not(feature = "no_module"))] - let hash_fn_def = { - if let Some(modules) = modules.as_mut() { + let hash_script = if let Some(modules) = modules.as_mut() { + modules.set_index(state.find_module(&modules.get(0).0)); + + // Rust functions are indexed in two steps: + // 1) Calculate a hash in a similar manner to script-defined functions, + // i.e. qualifiers + function name + number of arguments. + // 2) Calculate a second hash with no qualifiers, empty function name, + // zero number of arguments, and the actual list of argument `TypeId`'s. + // 3) The final hash is the XOR of the two hashes. + let qualifiers = modules.iter().map(|(m, _)| m.as_str()); + calc_fn_hash(qualifiers, &id, 0, empty()) + } else { + // Qualifiers (none) + function name + no parameters. + calc_fn_hash(empty(), &id, 0, empty()) + }; + + return Ok(Expr::FnCall(Box::new(( + (id.into(), false, settings.pos), + modules, + hash_script, + args, + None, + )))); + } + // id... + _ => (), + } + + let settings = settings.level_up(); + + loop { + match input.peek().unwrap() { + // id(...args, ) - handle trailing comma + (Token::RightParen, _) => (), + _ => args.push(parse_expr(input, state, settings)?), + } + + match input.peek().unwrap() { + // id(...args) + (Token::RightParen, _) => { + eat_token(input, Token::RightParen); + + let hash_script = if let Some(modules) = modules.as_mut() { modules.set_index(state.find_module(&modules.get(0).0)); // Rust functions are indexed in two steps: @@ -773,62 +857,16 @@ fn parse_call_expr<'a>( // zero number of arguments, and the actual list of argument `TypeId`'s. // 3) The final hash is the XOR of the two hashes. let qualifiers = modules.iter().map(|(m, _)| m.as_str()); - calc_fn_hash(qualifiers, &id, 0, empty()) + calc_fn_hash(qualifiers, &id, args.len(), empty()) } else { - // Qualifiers (none) + function name + no parameters. - calc_fn_hash(empty(), &id, 0, empty()) - } - }; - // Qualifiers (none) + function name + no parameters. - #[cfg(feature = "no_module")] - let hash_fn_def = calc_fn_hash(empty(), &id, 0, empty()); - - return Ok(Expr::FnCall(Box::new(( - (id.into(), false, begin), - modules, - hash_fn_def, - args, - None, - )))); - } - // id... - _ => (), - } - - loop { - args.push(parse_expr(input, state, level + 1, allow_stmt_expr)?); - - match input.peek().unwrap() { - // id(...args) - (Token::RightParen, _) => { - eat_token(input, Token::RightParen); - - #[cfg(not(feature = "no_module"))] - let hash_fn_def = { - if let Some(modules) = modules.as_mut() { - modules.set_index(state.find_module(&modules.get(0).0)); - - // Rust functions are indexed in two steps: - // 1) Calculate a hash in a similar manner to script-defined functions, - // i.e. qualifiers + function name + number of arguments. - // 2) Calculate a second hash with no qualifiers, empty function name, - // zero number of arguments, and the actual list of argument `TypeId`'s. - // 3) The final hash is the XOR of the two hashes. - let qualifiers = modules.iter().map(|(m, _)| m.as_str()); - calc_fn_hash(qualifiers, &id, args.len(), empty()) - } else { - // Qualifiers (none) + function name + number of arguments. - calc_fn_hash(empty(), &id, args.len(), empty()) - } + // Qualifiers (none) + function name + number of arguments. + calc_fn_hash(empty(), &id, args.len(), empty()) }; - // Qualifiers (none) + function name + number of arguments. - #[cfg(feature = "no_module")] - let hash_fn_def = calc_fn_hash(empty(), &id, args.len(), empty()); return Ok(Expr::FnCall(Box::new(( - (id.into(), false, begin), + (id.into(), false, settings.pos), modules, - hash_fn_def, + hash_script, args, None, )))); @@ -846,9 +884,7 @@ fn parse_call_expr<'a>( .into_err(*pos)) } // id(...args - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(*pos)) - } + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), // id(...args ??? (_, pos) => { return Err(PERR::MissingToken( @@ -863,19 +899,15 @@ fn parse_call_expr<'a>( /// Parse an indexing chain. /// Indexing binds to the right, so this call parses all possible levels of indexing following in the input. -fn parse_index_chain<'a>( - input: &mut Peekable>, +fn parse_index_chain( + input: &mut TokenStream, state: &mut ParseState, lhs: Expr, - pos: Position, - level: usize, - allow_stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.ensure_level_within_max_limit(state.max_expr_depth)?; - let idx_expr = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let idx_expr = parse_expr(input, state, settings.level_up())?; // Check type of indexing - must be integer or string match &idx_expr { @@ -1014,18 +1046,12 @@ fn parse_index_chain<'a>( match input.peek().unwrap() { // If another indexing level, right-bind it (Token::LeftBracket, _) => { - let idx_pos = eat_token(input, Token::LeftBracket); + let prev_pos = settings.pos; + settings.pos = eat_token(input, Token::LeftBracket); // Recursively parse the indexing chain, right-binding each - let idx_expr = parse_index_chain( - input, - state, - idx_expr, - idx_pos, - level + 1, - allow_stmt_expr, - )?; + let idx_expr = parse_index_chain(input, state, idx_expr, settings.level_up())?; // Indexing binds to right - Ok(Expr::Index(Box::new((lhs, idx_expr, pos)))) + Ok(Expr::Index(Box::new((lhs, idx_expr, prev_pos)))) } // Otherwise terminate the indexing chain _ => { @@ -1034,14 +1060,14 @@ fn parse_index_chain<'a>( // inside brackets to be mis-parsed as another level of indexing, or a // dot expression/function call to be mis-parsed as following the indexing chain. Expr::Index(_) | Expr::Dot(_) | Expr::FnCall(_) => Ok(Expr::Index( - Box::new((lhs, Expr::Expr(Box::new(idx_expr)), pos)), + Box::new((lhs, Expr::Expr(Box::new(idx_expr)), settings.pos)), )), - _ => Ok(Expr::Index(Box::new((lhs, idx_expr, pos)))), + _ => Ok(Expr::Index(Box::new((lhs, idx_expr, settings.pos)))), } } } } - (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(*pos)), + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), (_, pos) => Err(PERR::MissingToken( Token::RightBracket.into(), "for a matching [ in this index expression".into(), @@ -1051,137 +1077,147 @@ fn parse_index_chain<'a>( } /// Parse an array literal. -fn parse_array_literal<'a>( - input: &mut Peekable>, +fn parse_array_literal( + input: &mut TokenStream, state: &mut ParseState, - pos: Position, - level: usize, - allow_stmt_expr: bool, + settings: ParseSettings, ) -> Result { - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let mut arr = StaticVec::new(); - if !match_token(input, Token::RightBracket)? { - while !input.peek().unwrap().0.is_eof() { - arr.push(parse_expr(input, state, level + 1, allow_stmt_expr)?); - - match input.peek().unwrap() { - (Token::Comma, _) => eat_token(input, Token::Comma), - (Token::RightBracket, _) => { - eat_token(input, Token::RightBracket); - break; - } - (Token::EOF, pos) => { - return Err(PERR::MissingToken( - Token::RightBracket.into(), - "to end this array literal".into(), - ) - .into_err(*pos)) - } - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(*pos)) - } - (_, pos) => { - return Err(PERR::MissingToken( - Token::Comma.into(), - "to separate the items of this array literal".into(), - ) - .into_err(*pos)) - } - }; + while !input.peek().unwrap().0.is_eof() { + if state.max_array_size > 0 && arr.len() >= state.max_array_size { + return Err(PERR::LiteralTooLarge( + "Size of array literal".to_string(), + state.max_array_size, + ) + .into_err(input.peek().unwrap().1)); } + + match input.peek().unwrap() { + (Token::RightBracket, _) => { + eat_token(input, Token::RightBracket); + break; + } + _ => { + let expr = parse_expr(input, state, settings.level_up())?; + arr.push(expr); + } + } + + match input.peek().unwrap() { + (Token::Comma, _) => { + eat_token(input, Token::Comma); + } + (Token::RightBracket, _) => (), + (Token::EOF, pos) => { + return Err(PERR::MissingToken( + Token::RightBracket.into(), + "to end this array literal".into(), + ) + .into_err(*pos)) + } + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), + (_, pos) => { + return Err(PERR::MissingToken( + Token::Comma.into(), + "to separate the items of this array literal".into(), + ) + .into_err(*pos)) + } + }; } - Ok(Expr::Array(Box::new((arr, pos)))) + Ok(Expr::Array(Box::new((arr, settings.pos)))) } /// Parse a map literal. -fn parse_map_literal<'a>( - input: &mut Peekable>, +fn parse_map_literal( + input: &mut TokenStream, state: &mut ParseState, - pos: Position, - level: usize, - allow_stmt_expr: bool, + settings: ParseSettings, ) -> Result { - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let mut map = StaticVec::new(); - if !match_token(input, Token::RightBrace)? { - while !input.peek().unwrap().0.is_eof() { - const MISSING_RBRACE: &str = "to end this object map literal"; + while !input.peek().unwrap().0.is_eof() { + const MISSING_RBRACE: &str = "to end this object map literal"; - let (name, pos) = match input.next().unwrap() { - (Token::Identifier(s), pos) => (s, pos), - (Token::StringConst(s), pos) => (s, pos), - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).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.peek().unwrap() { + (Token::RightBrace, _) => { + eat_token(input, Token::RightBrace); + break; + } + _ => { + let (name, pos) = match input.next().unwrap() { + (Token::Identifier(s), pos) => (s, pos), + (Token::StringConst(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(PERR::BadInput(err.to_string()).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)) - } - }; + 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)) + } + }; - let expr = parse_expr(input, state, level + 1, allow_stmt_expr)?; - - map.push(((name, pos), expr)); - - match input.peek().unwrap() { - (Token::Comma, _) => { - eat_token(input, Token::Comma); - } - (Token::RightBrace, _) => { - eat_token(input, Token::RightBrace); - break; - } - (Token::Identifier(_), pos) => { - return Err(PERR::MissingToken( - Token::Comma.into(), - "to separate the items of this object map literal".into(), - ) - .into_err(*pos)) - } - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(*pos)) - } - (_, pos) => { - return Err( - PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) - .into_err(*pos), + if state.max_map_size > 0 && map.len() >= state.max_map_size { + return Err(PERR::LiteralTooLarge( + "Number of properties in object map literal".to_string(), + state.max_map_size, ) + .into_err(input.peek().unwrap().1)); } + + let expr = parse_expr(input, state, settings.level_up())?; + map.push(((name, pos), expr)); + } + } + + match input.peek().unwrap() { + (Token::Comma, _) => { + eat_token(input, Token::Comma); + } + (Token::RightBrace, _) => (), + (Token::Identifier(_), pos) => { + return Err(PERR::MissingToken( + Token::Comma.into(), + "to separate the items of this object map literal".into(), + ) + .into_err(*pos)) + } + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), + (_, pos) => { + return Err( + PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) + .into_err(*pos), + ) } } } @@ -1197,53 +1233,51 @@ fn parse_map_literal<'a>( }) .map_err(|(key, pos)| PERR::DuplicatedProperty(key.to_string()).into_err(pos))?; - Ok(Expr::Map(Box::new((map, pos)))) + Ok(Expr::Map(Box::new((map, settings.pos)))) } /// Parse a primary expression. -fn parse_primary<'a>( - input: &mut Peekable>, +fn parse_primary( + input: &mut TokenStream, state: &mut ParseState, - level: usize, - allow_stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { - let (token, pos) = input.peek().unwrap(); - let pos = *pos; - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + let (token, pos1) = input.peek().unwrap(); + settings.pos = *pos1; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let (token, _) = match token { // { - block statement as expression - Token::LeftBrace if allow_stmt_expr => { - return parse_block(input, state, false, level + 1, allow_stmt_expr) - .map(|block| Expr::Stmt(Box::new((block, pos)))); + Token::LeftBrace if settings.allow_stmt_expr => { + return parse_block(input, state, settings.level_up()) + .map(|block| Expr::Stmt(Box::new((block, settings.pos)))) } - Token::EOF => return Err(PERR::UnexpectedEOF.into_err(pos)), + Token::EOF => return Err(PERR::UnexpectedEOF.into_err(settings.pos)), _ => input.next().unwrap(), }; let mut root_expr = match token { - Token::IntegerConstant(x) => Expr::IntegerConstant(Box::new((x, pos))), + Token::IntegerConstant(x) => Expr::IntegerConstant(Box::new((x, settings.pos))), #[cfg(not(feature = "no_float"))] - Token::FloatConstant(x) => Expr::FloatConstant(Box::new((x, pos))), - Token::CharConstant(c) => Expr::CharConstant(Box::new((c, pos))), - Token::StringConst(s) => Expr::StringConstant(Box::new((s.into(), pos))), + Token::FloatConstant(x) => Expr::FloatConstant(Box::new((x, settings.pos))), + Token::CharConstant(c) => Expr::CharConstant(Box::new((c, settings.pos))), + Token::StringConst(s) => Expr::StringConstant(Box::new((s.into(), settings.pos))), Token::Identifier(s) => { let index = state.find(&s); - Expr::Variable(Box::new(((s, pos), None, 0, index))) + Expr::Variable(Box::new(((s, settings.pos), None, 0, index))) } - Token::LeftParen => parse_paren_expr(input, state, pos, level + 1, allow_stmt_expr)?, + Token::LeftParen => parse_paren_expr(input, state, settings.level_up())?, #[cfg(not(feature = "no_index"))] - Token::LeftBracket => parse_array_literal(input, state, pos, level + 1, allow_stmt_expr)?, + Token::LeftBracket => parse_array_literal(input, state, settings.level_up())?, #[cfg(not(feature = "no_object"))] - Token::MapStart => parse_map_literal(input, state, pos, level + 1, allow_stmt_expr)?, - Token::True => Expr::True(pos), - Token::False => Expr::False(pos), - Token::LexError(err) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), - token => { - return Err(PERR::BadInput(format!("Unexpected '{}'", token.syntax())).into_err(pos)) + Token::MapStart => parse_map_literal(input, state, settings.level_up())?, + Token::True => Expr::True(settings.pos), + Token::False => Expr::False(settings.pos), + Token::LexError(err) => return Err(err.into_err(settings.pos)), + _ => { + return Err( + PERR::BadInput(format!("Unexpected '{}'", token.syntax())).into_err(settings.pos) + ) } }; @@ -1256,19 +1290,21 @@ fn parse_primary<'a>( } let (token, token_pos) = input.next().unwrap(); + settings.pos = token_pos; root_expr = match (root_expr, token) { // Function call (Expr::Variable(x), Token::LeftParen) => { let ((name, pos), modules, _, _) = *x; - parse_call_expr(input, state, name, modules, pos, level + 1, allow_stmt_expr)? + settings.pos = pos; + parse_call_expr(input, state, name, modules, settings.level_up())? } (Expr::Property(_), _) => unreachable!(), // module access - #[cfg(not(feature = "no_module"))] (Expr::Variable(x), Token::DoubleColon) => match input.next().unwrap() { (Token::Identifier(id2), pos2) => { let ((name, pos), mut modules, _, index) = *x; + if let Some(ref mut modules) = modules { modules.push((name, pos)); } else { @@ -1284,16 +1320,19 @@ fn parse_primary<'a>( // Indexing #[cfg(not(feature = "no_index"))] (expr, Token::LeftBracket) => { - parse_index_chain(input, state, expr, token_pos, level + 1, allow_stmt_expr)? + parse_index_chain(input, state, expr, settings.level_up())? } // Unknown postfix operator - (expr, token) => panic!("unknown postfix operator {:?} for {:?}", token, expr), + (expr, token) => unreachable!( + "unknown postfix operator '{}' for {:?}", + token.syntax(), + expr + ), } } match &mut root_expr { // Cache the hash key for module-qualified variables - #[cfg(not(feature = "no_module"))] Expr::Variable(x) if x.1.is_some() => { let ((name, _), modules, hash, _) = x.as_mut(); let modules = modules.as_mut().unwrap(); @@ -1309,30 +1348,26 @@ fn parse_primary<'a>( } /// Parse a potential unary operator. -fn parse_unary<'a>( - input: &mut Peekable>, +fn parse_unary( + input: &mut TokenStream, state: &mut ParseState, - level: usize, - allow_stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { - let (token, pos) = input.peek().unwrap(); - let pos = *pos; - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + let (token, token_pos) = input.peek().unwrap(); + settings.pos = *token_pos; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; match token { // If statement is allowed to act as expressions - Token::If => Ok(Expr::Stmt(Box::new(( - parse_if(input, state, false, level + 1, allow_stmt_expr)?, - pos, + Token::If if settings.allow_if_expr => Ok(Expr::Stmt(Box::new(( + parse_if(input, state, settings.level_up())?, + settings.pos, )))), // -expr Token::UnaryMinus => { let pos = eat_token(input, Token::UnaryMinus); - match parse_unary(input, state, level + 1, allow_stmt_expr)? { + match parse_unary(input, state, settings.level_up())? { // Negative integer Expr::IntegerConstant(x) => { let (num, pos) = *x; @@ -1349,12 +1384,7 @@ fn parse_unary<'a>( None } }) - .ok_or_else(|| { - PERR::BadInput( - LexError::MalformedNumber(format!("-{}", x.0)).to_string(), - ) - .into_err(pos) - }) + .ok_or_else(|| LexError::MalformedNumber(format!("-{}", x.0)).into_err(pos)) } // Negative float @@ -1381,13 +1411,14 @@ fn parse_unary<'a>( // +expr Token::UnaryPlus => { eat_token(input, Token::UnaryPlus); - parse_unary(input, state, level + 1, allow_stmt_expr) + parse_unary(input, state, settings.level_up()) } // !expr Token::Bang => { let pos = eat_token(input, Token::Bang); let mut args = StaticVec::new(); - args.push(parse_primary(input, state, level + 1, allow_stmt_expr)?); + let expr = parse_primary(input, state, settings.level_up())?; + args.push(expr); let op = "!"; let hash = calc_fn_hash(empty(), op, 2, empty()); @@ -1401,9 +1432,9 @@ fn parse_unary<'a>( )))) } // - Token::EOF => Err(PERR::UnexpectedEOF.into_err(pos)), + Token::EOF => Err(PERR::UnexpectedEOF.into_err(settings.pos)), // All other tokens - _ => parse_primary(input, state, level + 1, allow_stmt_expr), + _ => parse_primary(input, state, settings.level_up()), } } @@ -1415,9 +1446,11 @@ fn make_assignment_stmt<'a>( pos: Position, ) -> Result { match &lhs { + // var (non-indexed) = rhs Expr::Variable(x) if x.3.is_none() => { Ok(Expr::Assignment(Box::new((lhs, fn_name.into(), rhs, pos)))) } + // var (indexed) = rhs Expr::Variable(x) => { let ((name, name_pos), _, _, index) = x.as_ref(); match state.stack[(state.len() - index.unwrap().get())].1 { @@ -1431,10 +1464,13 @@ fn make_assignment_stmt<'a>( ScopeEntryType::Module => unreachable!(), } } + // xxx[???] = rhs, xxx.??? = rhs Expr::Index(x) | Expr::Dot(x) => match &x.0 { + // var[???] (non-indexed) = rhs, var.??? (non-indexed) = rhs Expr::Variable(x) if x.3.is_none() => { Ok(Expr::Assignment(Box::new((lhs, fn_name.into(), rhs, pos)))) } + // var[???] (indexed) = rhs, var.??? (indexed) = rhs Expr::Variable(x) => { let ((name, name_pos), _, _, index) = x.as_ref(); match state.stack[(state.len() - index.unwrap().get())].1 { @@ -1448,29 +1484,32 @@ fn make_assignment_stmt<'a>( ScopeEntryType::Module => unreachable!(), } } + // expr[???] = rhs, expr.??? = rhs _ => Err(PERR::AssignmentToCopy.into_err(x.0.position())), }, + // const_expr = rhs expr if expr.is_constant() => { Err(PERR::AssignmentToConstant("".into()).into_err(lhs.position())) } + // ??? && ??? = rhs, ??? || ??? = rhs + Expr::And(_) | Expr::Or(_) => { + Err(PERR::BadInput("Possibly a typo of '=='?".to_string()).into_err(pos)) + } + // expr = rhs _ => Err(PERR::AssignmentToCopy.into_err(lhs.position())), } } /// Parse an operator-assignment expression. -fn parse_op_assignment_stmt<'a>( - input: &mut Peekable>, +fn parse_op_assignment_stmt( + input: &mut TokenStream, state: &mut ParseState, lhs: Expr, - level: usize, - allow_stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { - let (token, pos) = input.peek().unwrap(); - let pos = *pos; - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + let (token, token_pos) = input.peek().unwrap(); + settings.pos = *token_pos; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let op = match token { Token::Equals => "".into(), @@ -1491,7 +1530,7 @@ fn parse_op_assignment_stmt<'a>( }; let (_, pos) = input.next().unwrap(); - let rhs = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let rhs = parse_expr(input, state, settings.level_up())?; make_assignment_stmt(op, state, lhs, rhs, pos) } @@ -1520,9 +1559,6 @@ fn make_dot_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { - #[cfg(feature = "no_module")] - unreachable!(); - #[cfg(not(feature = "no_module"))] return Err(PERR::PropertyExpected.into_err(x.1.unwrap().get(0).1)); } // lhs.prop @@ -1703,17 +1739,15 @@ fn make_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result( - input: &mut Peekable>, +fn parse_binary_op( + input: &mut TokenStream, state: &mut ParseState, parent_precedence: u8, lhs: Expr, - mut level: usize, - allow_stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(lhs.position())); - } + settings.pos = lhs.position(); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let mut root = lhs; @@ -1730,24 +1764,22 @@ fn parse_binary_op<'a>( let (op_token, pos) = input.next().unwrap(); - let rhs = parse_unary(input, state, level, allow_stmt_expr)?; + let rhs = parse_unary(input, state, settings)?; let next_precedence = input.peek().unwrap().0.precedence(); // Bind to right if the next operator has higher precedence // If same precedence, then check if the operator binds right let rhs = if (precedence == next_precedence && bind_right) || precedence < next_precedence { - parse_binary_op(input, state, precedence, rhs, level, allow_stmt_expr)? + parse_binary_op(input, state, precedence, rhs, settings)? } else { // Otherwise bind to left (even if next operator has the same precedence) rhs }; - level += 1; - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings = settings.level_up(); + settings.pos = pos; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let cmp_def = Some(false.into()); let op = op_token.syntax(); @@ -1815,33 +1847,26 @@ fn parse_binary_op<'a>( make_dot_expr(current_lhs, rhs, pos)? } - token => return Err(PERR::UnknownOperator(token.into()).into_err(pos)), + op_token => return Err(PERR::UnknownOperator(op_token.into()).into_err(pos)), }; } } /// Parse an expression. -fn parse_expr<'a>( - input: &mut Peekable>, +fn parse_expr( + input: &mut TokenStream, state: &mut ParseState, - level: usize, - allow_stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { - let (_, pos) = input.peek().unwrap(); + settings.pos = input.peek().unwrap().1; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(*pos)); - } - - let lhs = parse_unary(input, state, level + 1, allow_stmt_expr)?; - parse_binary_op(input, state, 1, lhs, level + 1, allow_stmt_expr) + let lhs = parse_unary(input, state, settings.level_up())?; + parse_binary_op(input, state, 1, lhs, settings.level_up()) } /// Make sure that the expression is not a statement expression (i.e. wrapped in `{}`). -fn ensure_not_statement_expr<'a>( - input: &mut Peekable>, - type_name: &str, -) -> Result<(), ParseError> { +fn ensure_not_statement_expr(input: &mut TokenStream, type_name: &str) -> Result<(), ParseError> { match input.peek().unwrap() { // Disallow statement expressions (Token::LeftBrace, pos) | (Token::EOF, pos) => { @@ -1853,7 +1878,7 @@ fn ensure_not_statement_expr<'a>( } /// Make sure that the expression is not a mis-typed assignment (i.e. `a = b` instead of `a == b`). -fn ensure_not_assignment<'a>(input: &mut Peekable>) -> Result<(), ParseError> { +fn ensure_not_assignment(input: &mut TokenStream) -> Result<(), ParseError> { match input.peek().unwrap() { (Token::Equals, pos) => { return Err(PERR::BadInput("Possibly a typo of '=='?".to_string()).into_err(*pos)) @@ -1880,34 +1905,29 @@ fn ensure_not_assignment<'a>(input: &mut Peekable>) -> Result< } /// Parse an if statement. -fn parse_if<'a>( - input: &mut Peekable>, +fn parse_if( + input: &mut TokenStream, state: &mut ParseState, - breakable: bool, - level: usize, - allow_stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { // if ... - let pos = eat_token(input, Token::If); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.pos = eat_token(input, Token::If); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; // if guard { if_body } ensure_not_statement_expr(input, "a boolean")?; - let guard = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let guard = parse_expr(input, state, settings.level_up())?; ensure_not_assignment(input)?; - let if_body = parse_block(input, state, breakable, level + 1, allow_stmt_expr)?; + let if_body = parse_block(input, state, settings.level_up())?; // if guard { if_body } else ... let else_body = if match_token(input, Token::Else).unwrap_or(false) { Some(if let (Token::If, _) = input.peek().unwrap() { // if guard { if_body } else if ... - parse_if(input, state, breakable, level + 1, allow_stmt_expr)? + parse_if(input, state, settings.level_up())? } else { // if guard { if_body } else { else-body } - parse_block(input, state, breakable, level + 1, allow_stmt_expr)? + parse_block(input, state, settings.level_up())? }) } else { None @@ -1917,68 +1937,59 @@ fn parse_if<'a>( } /// Parse a while loop. -fn parse_while<'a>( - input: &mut Peekable>, +fn parse_while( + input: &mut TokenStream, state: &mut ParseState, - level: usize, - allow_stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { // while ... - let pos = eat_token(input, Token::While); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.pos = eat_token(input, Token::While); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; // while guard { body } ensure_not_statement_expr(input, "a boolean")?; - let guard = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let guard = parse_expr(input, state, settings.level_up())?; ensure_not_assignment(input)?; - let body = parse_block(input, state, true, level + 1, allow_stmt_expr)?; + + settings.is_breakable = true; + let body = parse_block(input, state, settings.level_up())?; Ok(Stmt::While(Box::new((guard, body)))) } /// Parse a loop statement. -fn parse_loop<'a>( - input: &mut Peekable>, +fn parse_loop( + input: &mut TokenStream, state: &mut ParseState, - level: usize, - allow_stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { // loop ... - let pos = eat_token(input, Token::Loop); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.pos = eat_token(input, Token::Loop); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; // loop { body } - let body = parse_block(input, state, true, level + 1, allow_stmt_expr)?; + settings.is_breakable = true; + let body = parse_block(input, state, settings.level_up())?; Ok(Stmt::Loop(Box::new(body))) } /// Parse a for loop. -fn parse_for<'a>( - input: &mut Peekable>, +fn parse_for( + input: &mut TokenStream, state: &mut ParseState, - level: usize, - allow_stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { // for ... - let pos = eat_token(input, Token::For); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.pos = eat_token(input, Token::For); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; // for name ... let name = match input.next().unwrap() { // Variable name (Token::Identifier(s), _) => s, // Bad identifier - (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).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 @@ -1988,7 +1999,7 @@ fn parse_for<'a>( // for name in ... match input.next().unwrap() { (Token::In, _) => (), - (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => { return Err( PERR::MissingToken(Token::In.into(), "after the iteration variable".into()) @@ -1999,12 +2010,13 @@ fn parse_for<'a>( // for name in expr { body } ensure_not_statement_expr(input, "a boolean")?; - let expr = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let expr = parse_expr(input, state, settings.level_up())?; let prev_len = state.len(); state.push((name.clone(), ScopeEntryType::Normal)); - let body = parse_block(input, state, true, level + 1, allow_stmt_expr)?; + settings.is_breakable = true; + let body = parse_block(input, state, settings.level_up())?; state.truncate(prev_len); @@ -2012,31 +2024,27 @@ fn parse_for<'a>( } /// Parse a variable definition statement. -fn parse_let<'a>( - input: &mut Peekable>, +fn parse_let( + input: &mut TokenStream, state: &mut ParseState, var_type: ScopeEntryType, - level: usize, - allow_stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { // let/const... (specified in `var_type`) - let (_, pos) = input.next().unwrap(); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.pos = input.next().unwrap().1; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; // let name ... let (name, pos) = match input.next().unwrap() { (Token::Identifier(s), pos) => (s, pos), - (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; // let name = ... if match_token(input, Token::Equals)? { // let name = expr - let init_value = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let init_value = parse_expr(input, state, settings.level_up())?; match var_type { // let name = expr @@ -2074,21 +2082,17 @@ fn parse_let<'a>( } /// Parse an import statement. -fn parse_import<'a>( - input: &mut Peekable>, +fn parse_import( + input: &mut TokenStream, state: &mut ParseState, - level: usize, - allow_stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { // import ... - let pos = eat_token(input, Token::Import); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.pos = eat_token(input, Token::Import); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; // import expr ... - let expr = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let expr = parse_expr(input, state, settings.level_up())?; // import expr as ... match input.next().unwrap() { @@ -2104,34 +2108,30 @@ fn parse_import<'a>( // import expr as name ... let (name, _) = match input.next().unwrap() { (Token::Identifier(s), pos) => (s, pos), - (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; state.push((name.clone(), ScopeEntryType::Module)); - Ok(Stmt::Import(Box::new((expr, (name, pos))))) + Ok(Stmt::Import(Box::new((expr, (name, settings.pos))))) } /// Parse an export statement. -fn parse_export<'a>( - input: &mut Peekable>, +#[cfg(not(feature = "no_module"))] +fn parse_export( + input: &mut TokenStream, state: &mut ParseState, - level: usize, + mut settings: ParseSettings, ) -> Result { - let pos = eat_token(input, Token::Export); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.pos = eat_token(input, Token::Export); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let mut exports = StaticVec::new(); loop { let (id, id_pos) = match input.next().unwrap() { (Token::Identifier(s), pos) => (s.clone(), pos), - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(pos)) - } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; @@ -2178,17 +2178,15 @@ fn parse_export<'a>( } /// Parse a statement block. -fn parse_block<'a>( - input: &mut Peekable>, +fn parse_block( + input: &mut TokenStream, state: &mut ParseState, - breakable: bool, - level: usize, - allow_stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { // Must start with { - let pos = match input.next().unwrap() { + settings.pos = match input.next().unwrap() { (Token::LeftBrace, pos) => pos, - (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => { return Err(PERR::MissingToken( Token::LeftBrace.into(), @@ -2198,16 +2196,15 @@ fn parse_block<'a>( } }; - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let mut statements = StaticVec::new(); let prev_len = state.len(); while !match_token(input, Token::RightBrace)? { // Parse statements inside the block - let stmt = parse_stmt(input, state, breakable, false, level + 1, allow_stmt_expr)?; + settings.is_global = false; + let stmt = parse_stmt(input, state, settings.level_up())?; // See if it needs a terminating semicolon let need_semicolon = !stmt.is_self_terminated(); @@ -2229,9 +2226,7 @@ fn parse_block<'a>( // { ... { stmt } ??? (_, _) if !need_semicolon => (), // { ... stmt - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(*pos)) - } + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), // { ... stmt ??? (_, pos) => { // Semicolons are not optional between statements @@ -2246,91 +2241,83 @@ fn parse_block<'a>( state.truncate(prev_len); - Ok(Stmt::Block(Box::new((statements, pos)))) + Ok(Stmt::Block(Box::new((statements, settings.pos)))) } /// Parse an expression as a statement. -fn parse_expr_stmt<'a>( - input: &mut Peekable>, +fn parse_expr_stmt( + input: &mut TokenStream, state: &mut ParseState, - level: usize, - allow_stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { - let (_, pos) = input.peek().unwrap(); + settings.pos = input.peek().unwrap().1; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(*pos)); - } - - let expr = parse_expr(input, state, level + 1, allow_stmt_expr)?; - let expr = parse_op_assignment_stmt(input, state, expr, level + 1, allow_stmt_expr)?; + let expr = parse_expr(input, state, settings.level_up())?; + let expr = parse_op_assignment_stmt(input, state, expr, settings.level_up())?; Ok(Stmt::Expr(Box::new(expr))) } /// Parse a single statement. -fn parse_stmt<'a>( - input: &mut Peekable>, +fn parse_stmt( + input: &mut TokenStream, state: &mut ParseState, - breakable: bool, - is_global: bool, - level: usize, - allow_stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { use ScopeEntryType::{Constant, Normal}; - let (token, pos) = match input.peek().unwrap() { + let (token, token_pos) = match input.peek().unwrap() { (Token::EOF, pos) => return Ok(Stmt::Noop(*pos)), x => x, }; - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(*pos)); - } + settings.pos = *token_pos; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; match token { // Semicolon - empty statement - Token::SemiColon => Ok(Stmt::Noop(*pos)), + Token::SemiColon => Ok(Stmt::Noop(settings.pos)), - Token::LeftBrace => parse_block(input, state, breakable, level + 1, allow_stmt_expr), + Token::LeftBrace => parse_block(input, state, settings.level_up()), // fn ... - Token::Fn if !is_global => Err(PERR::WrongFnDefinition.into_err(*pos)), + #[cfg(not(feature = "no_function"))] + Token::Fn if !settings.is_global => Err(PERR::WrongFnDefinition.into_err(settings.pos)), + #[cfg(not(feature = "no_function"))] Token::Fn => unreachable!(), - Token::If => parse_if(input, state, breakable, level + 1, allow_stmt_expr), - Token::While => parse_while(input, state, level + 1, allow_stmt_expr), - Token::Loop => parse_loop(input, state, level + 1, allow_stmt_expr), - Token::For => parse_for(input, state, level + 1, allow_stmt_expr), + Token::If => parse_if(input, state, settings.level_up()), + Token::While => parse_while(input, state, settings.level_up()), + Token::Loop => parse_loop(input, state, settings.level_up()), + Token::For => parse_for(input, state, settings.level_up()), - Token::Continue if breakable => { + Token::Continue if settings.is_breakable => { let pos = eat_token(input, Token::Continue); Ok(Stmt::Continue(pos)) } - Token::Break if breakable => { + Token::Break if settings.is_breakable => { let pos = eat_token(input, Token::Break); Ok(Stmt::Break(pos)) } - Token::Continue | Token::Break => Err(PERR::LoopBreak.into_err(*pos)), + Token::Continue | Token::Break => Err(PERR::LoopBreak.into_err(settings.pos)), Token::Return | Token::Throw => { - let pos = *pos; - let return_type = match input.next().unwrap() { (Token::Return, _) => ReturnType::Return, (Token::Throw, _) => ReturnType::Exception, - _ => panic!("token should be return or throw"), + _ => unreachable!(), }; match input.peek().unwrap() { // `return`/`throw` at (Token::EOF, pos) => Ok(Stmt::ReturnWithVal(Box::new(((return_type, *pos), None)))), // `return;` or `throw;` - (Token::SemiColon, _) => { - Ok(Stmt::ReturnWithVal(Box::new(((return_type, pos), None)))) - } + (Token::SemiColon, _) => Ok(Stmt::ReturnWithVal(Box::new(( + (return_type, settings.pos), + None, + )))), // `return` or `throw` with expression (_, _) => { - let expr = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let expr = parse_expr(input, state, settings.level_up())?; let pos = expr.position(); Ok(Stmt::ReturnWithVal(Box::new(( @@ -2341,35 +2328,30 @@ fn parse_stmt<'a>( } } - Token::Let => parse_let(input, state, Normal, level + 1, allow_stmt_expr), - Token::Const => parse_let(input, state, Constant, level + 1, allow_stmt_expr), + Token::Let => parse_let(input, state, Normal, settings.level_up()), + Token::Const => parse_let(input, state, Constant, settings.level_up()), + Token::Import => parse_import(input, state, settings.level_up()), #[cfg(not(feature = "no_module"))] - Token::Import => parse_import(input, state, level + 1, allow_stmt_expr), + Token::Export if !settings.is_global => Err(PERR::WrongExport.into_err(settings.pos)), #[cfg(not(feature = "no_module"))] - Token::Export if !is_global => Err(PERR::WrongExport.into_err(*pos)), + Token::Export => parse_export(input, state, settings.level_up()), - #[cfg(not(feature = "no_module"))] - Token::Export => parse_export(input, state, level + 1), - - _ => parse_expr_stmt(input, state, level + 1, allow_stmt_expr), + _ => parse_expr_stmt(input, state, settings.level_up()), } } /// Parse a function definition. -fn parse_fn<'a>( - input: &mut Peekable>, +#[cfg(not(feature = "no_function"))] +fn parse_fn( + input: &mut TokenStream, state: &mut ParseState, access: FnAccess, - level: usize, - allow_stmt_expr: bool, -) -> Result { - let pos = eat_token(input, Token::Fn); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + mut settings: ParseSettings, +) -> Result { + settings.pos = eat_token(input, Token::Fn); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let name = match input.next().unwrap() { (Token::Identifier(s), _) => s, @@ -2388,17 +2370,20 @@ fn parse_fn<'a>( let sep_err = format!("to separate the parameters of function '{}'", name); loop { - match input.next().unwrap() { - (Token::Identifier(s), pos) => { - state.push((s.clone(), ScopeEntryType::Normal)); - params.push((s, pos)) - } - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(pos)) - } - (_, pos) => { - return Err(PERR::MissingToken(Token::RightParen.into(), end_err).into_err(pos)) - } + match input.peek().unwrap() { + (Token::RightParen, _) => (), + _ => match input.next().unwrap() { + (Token::Identifier(s), pos) => { + state.push((s.clone(), ScopeEntryType::Normal)); + params.push((s, pos)) + } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (_, pos) => { + return Err( + PERR::MissingToken(Token::RightParen.into(), end_err).into_err(pos) + ) + } + }, } match input.next().unwrap() { @@ -2407,9 +2392,7 @@ fn parse_fn<'a>( (Token::Identifier(_), pos) => { return Err(PERR::MissingToken(Token::Comma.into(), sep_err).into_err(pos)) } - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(pos)) - } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => { return Err(PERR::MissingToken(Token::Comma.into(), sep_err).into_err(pos)) } @@ -2434,142 +2417,182 @@ fn parse_fn<'a>( // Parse function body let body = match input.peek().unwrap() { - (Token::LeftBrace, _) => parse_block(input, state, false, level + 1, allow_stmt_expr)?, + (Token::LeftBrace, _) => { + settings.is_breakable = false; + parse_block(input, state, settings.level_up())? + } (_, pos) => return Err(PERR::FnMissingBody(name).into_err(*pos)), }; let params = params.into_iter().map(|(p, _)| p).collect(); - Ok(FnDef { + Ok(ScriptFnDef { name, access, params, body, - pos, + pos: settings.pos, }) } -pub fn parse_global_expr<'a>( - input: &mut Peekable>, - engine: &Engine, - scope: &Scope, - optimization_level: OptimizationLevel, - max_expr_depth: usize, -) -> Result { - let mut state = ParseState::new(max_expr_depth); - let expr = parse_expr(input, &mut state, 0, false)?; - - match input.peek().unwrap() { - (Token::EOF, _) => (), - // Return error if the expression doesn't end - (token, pos) => { - return Err(PERR::BadInput(format!("Unexpected '{}'", token.syntax())).into_err(*pos)) - } - } - - Ok( - // Optimize AST - optimize_into_ast( - engine, - scope, - vec![Stmt::Expr(Box::new(expr))], - vec![], - optimization_level, - ), - ) -} - -/// Parse the global level statements. -fn parse_global_level<'a>( - input: &mut Peekable>, - max_expr_depth: (usize, usize), -) -> Result<(Vec, Vec), ParseError> { - let mut statements = Vec::::new(); - let mut functions = HashMap::::with_hasher(StraightHasherBuilder); - let mut state = ParseState::new(max_expr_depth.0); - - while !input.peek().unwrap().0.is_eof() { - // Collect all the function definitions - #[cfg(not(feature = "no_function"))] - { - let (access, must_be_fn) = if match_token(input, Token::Private)? { - (FnAccess::Private, true) - } else { - (FnAccess::Public, false) - }; - - match input.peek().unwrap() { - (Token::Fn, _) => { - let mut state = ParseState::new(max_expr_depth.1); - let func = parse_fn(input, &mut state, access, 0, true)?; - - // Qualifiers (none) + function name + number of arguments. - let hash = calc_fn_hash(empty(), &func.name, func.params.len(), empty()); - - functions.insert(hash, func); - continue; - } - (_, pos) if must_be_fn => { - return Err(PERR::MissingToken( - Token::Fn.into(), - format!("following '{}'", Token::Private.syntax()), - ) - .into_err(*pos)) - } - _ => (), - } - } - // Actual statement - let stmt = parse_stmt(input, &mut state, false, true, 0, true)?; - - let need_semicolon = !stmt.is_self_terminated(); - - statements.push(stmt); +impl Engine { + pub(crate) fn parse_global_expr( + &self, + input: &mut TokenStream, + scope: &Scope, + optimization_level: OptimizationLevel, + ) -> Result { + let mut state = ParseState::new( + self.max_expr_depth, + self.max_string_size, + self.max_array_size, + self.max_map_size, + ); + let settings = ParseSettings { + allow_if_expr: false, + allow_stmt_expr: false, + is_global: true, + is_breakable: false, + level: 0, + pos: Position::none(), + }; + let expr = parse_expr(input, &mut state, settings)?; match input.peek().unwrap() { - // EOF - (Token::EOF, _) => break, - // stmt ; - (Token::SemiColon, _) if need_semicolon => { - eat_token(input, Token::SemiColon); - } - // stmt ; - (Token::SemiColon, _) if !need_semicolon => (), - // { stmt } ??? - (_, _) if !need_semicolon => (), - // stmt - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(*pos)) - } - // stmt ??? - (_, pos) => { - // Semicolons are not optional between statements - return Err(PERR::MissingToken( - Token::SemiColon.into(), - "to terminate this statement".into(), + (Token::EOF, _) => (), + // Return error if the expression doesn't end + (token, pos) => { + return Err( + PERR::BadInput(format!("Unexpected '{}'", token.syntax())).into_err(*pos) ) - .into_err(*pos)); } } + + let expr = vec![Stmt::Expr(Box::new(expr))]; + + Ok( + // Optimize AST + optimize_into_ast(self, scope, expr, Default::default(), optimization_level), + ) } - Ok((statements, functions.into_iter().map(|(_, v)| v).collect())) -} + /// Parse the global level statements. + fn parse_global_level( + &self, + input: &mut TokenStream, + ) -> Result<(Vec, Vec), ParseError> { + let mut statements = Vec::::new(); + let mut functions = HashMap::::with_hasher(StraightHasherBuilder); + let mut state = ParseState::new( + self.max_expr_depth, + self.max_string_size, + self.max_array_size, + self.max_map_size, + ); -/// Run the parser on an input stream, returning an AST. -pub fn parse<'a>( - input: &mut Peekable>, - engine: &Engine, - scope: &Scope, - optimization_level: OptimizationLevel, - max_expr_depth: (usize, usize), -) -> Result { - let (statements, lib) = parse_global_level(input, max_expr_depth)?; + while !input.peek().unwrap().0.is_eof() { + // Collect all the function definitions + #[cfg(not(feature = "no_function"))] + { + let (access, must_be_fn) = if match_token(input, Token::Private)? { + (FnAccess::Private, true) + } else { + (FnAccess::Public, false) + }; - Ok( - // Optimize AST - optimize_into_ast(engine, scope, statements, lib, optimization_level), - ) + match input.peek().unwrap() { + #[cfg(not(feature = "no_function"))] + (Token::Fn, pos) => { + let mut state = ParseState::new( + self.max_function_expr_depth, + self.max_string_size, + self.max_array_size, + self.max_map_size, + ); + let settings = ParseSettings { + allow_if_expr: true, + allow_stmt_expr: true, + is_global: false, + is_breakable: false, + level: 0, + pos: *pos, + }; + let func = parse_fn(input, &mut state, access, settings)?; + + // Qualifiers (none) + function name + number of arguments. + let hash = calc_fn_hash(empty(), &func.name, func.params.len(), empty()); + + functions.insert(hash, func); + continue; + } + (_, pos) if must_be_fn => { + return Err(PERR::MissingToken( + Token::Fn.into(), + format!("following '{}'", Token::Private.syntax()), + ) + .into_err(*pos)) + } + _ => (), + } + } + + // Actual statement + let settings = ParseSettings { + allow_if_expr: true, + allow_stmt_expr: true, + is_global: true, + is_breakable: false, + level: 0, + pos: Position::none(), + }; + let stmt = parse_stmt(input, &mut state, settings)?; + + let need_semicolon = !stmt.is_self_terminated(); + + statements.push(stmt); + + match input.peek().unwrap() { + // EOF + (Token::EOF, _) => break, + // stmt ; + (Token::SemiColon, _) if need_semicolon => { + eat_token(input, Token::SemiColon); + } + // stmt ; + (Token::SemiColon, _) if !need_semicolon => (), + // { stmt } ??? + (_, _) if !need_semicolon => (), + // stmt + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), + // stmt ??? + (_, pos) => { + // Semicolons are not optional between statements + return Err(PERR::MissingToken( + Token::SemiColon.into(), + "to terminate this statement".into(), + ) + .into_err(*pos)); + } + } + } + + Ok((statements, functions.into_iter().map(|(_, v)| v).collect())) + } + + /// Run the parser on an input stream, returning an AST. + pub(crate) fn parse( + &self, + input: &mut TokenStream, + scope: &Scope, + optimization_level: OptimizationLevel, + ) -> Result { + let (statements, lib) = self.parse_global_level(input)?; + + Ok( + // Optimize AST + optimize_into_ast(self, scope, statements, lib, optimization_level), + ) + } } /// Map a `Dynamic` value to an expression. diff --git a/src/result.rs b/src/result.rs index 2589016b..760a9a5e 100644 --- a/src/result.rs +++ b/src/result.rs @@ -1,7 +1,7 @@ //! Module containing error definitions for the evaluation process. use crate::any::Dynamic; -use crate::error::ParseError; +use crate::error::ParseErrorType; use crate::parser::INT; use crate::token::Position; @@ -13,6 +13,7 @@ use crate::stdlib::{ }; #[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] use crate::stdlib::path::PathBuf; /// Evaluation result. @@ -23,12 +24,13 @@ use crate::stdlib::path::PathBuf; #[derive(Debug)] pub enum EvalAltResult { /// Syntax error. - ErrorParsing(ParseError), + ErrorParsing(ParseErrorType, Position), /// Error reading from a script file. Wrapped value is the path of the script file. /// /// Never appears under the `no_std` feature. #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] ErrorReadingScriptFile(PathBuf, Position, std::io::Error), /// Call to an unknown function. Wrapped value is the name of the function. @@ -81,6 +83,8 @@ pub enum EvalAltResult { ErrorTooManyModules(Position), /// Call stack over maximum limit. ErrorStackOverflow(Position), + /// Data value over maximum size limit. Wrapped values are the data type, maximum size and current size. + ErrorDataTooLarge(String, usize, usize, Position), /// The script is prematurely terminated. ErrorTerminated(Position), /// Run-time error encountered. Wrapped value is the error message. @@ -99,9 +103,10 @@ impl EvalAltResult { pub(crate) fn desc(&self) -> &str { match self { #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] Self::ErrorReadingScriptFile(_, _, _) => "Cannot read from script file", - Self::ErrorParsing(p) => p.desc(), + Self::ErrorParsing(p, _) => p.desc(), Self::ErrorInFunctionCall(_, _, _) => "Error in called function", Self::ErrorFunctionNotFound(_, _) => "Function not found", Self::ErrorBooleanArgMismatch(_, _) => "Boolean operator expects boolean operands", @@ -139,6 +144,7 @@ impl EvalAltResult { Self::ErrorTooManyOperations(_) => "Too many operations", Self::ErrorTooManyModules(_) => "Too many modules imported", Self::ErrorStackOverflow(_) => "Stack overflow", + Self::ErrorDataTooLarge(_, _, _, _) => "Data size exceeds maximum limit", Self::ErrorTerminated(_) => "Script terminated.", Self::ErrorRuntime(_, _) => "Runtime error", Self::ErrorLoopBreak(true, _) => "Break statement not inside a loop", @@ -153,95 +159,93 @@ impl Error for EvalAltResult {} impl fmt::Display for EvalAltResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let desc = self.desc(); + let pos = self.position(); match self { #[cfg(not(feature = "no_std"))] - Self::ErrorReadingScriptFile(path, pos, err) if pos.is_none() => { - write!(f, "{} '{}': {}", desc, path.display(), err) - } - #[cfg(not(feature = "no_std"))] - Self::ErrorReadingScriptFile(path, pos, err) => { - write!(f, "{} '{}': {} ({})", desc, path.display(), err, pos) + #[cfg(not(target_arch = "wasm32"))] + Self::ErrorReadingScriptFile(path, _, err) => { + write!(f, "{} '{}': {}", desc, path.display(), err)? } - Self::ErrorParsing(p) => write!(f, "Syntax error: {}", p), + Self::ErrorParsing(p, _) => write!(f, "Syntax error: {}", p)?, - Self::ErrorInFunctionCall(s, err, pos) => { - write!(f, "Error in call to function '{}' ({}): {}", s, pos, err) + Self::ErrorInFunctionCall(s, err, _) => { + write!(f, "Error in call to function '{}' : {}", s, err)? } - Self::ErrorFunctionNotFound(s, pos) - | Self::ErrorVariableNotFound(s, pos) - | Self::ErrorModuleNotFound(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos), + Self::ErrorFunctionNotFound(s, _) + | Self::ErrorVariableNotFound(s, _) + | Self::ErrorModuleNotFound(s, _) => write!(f, "{}: '{}'", desc, s)?, - Self::ErrorDotExpr(s, pos) if !s.is_empty() => write!(f, "{} {} ({})", desc, s, pos), + Self::ErrorDotExpr(s, _) if !s.is_empty() => write!(f, "{} {}", desc, s)?, - Self::ErrorIndexingType(_, pos) - | Self::ErrorNumericIndexExpr(pos) - | Self::ErrorStringIndexExpr(pos) - | Self::ErrorImportExpr(pos) - | Self::ErrorLogicGuard(pos) - | Self::ErrorFor(pos) - | Self::ErrorAssignmentToUnknownLHS(pos) - | Self::ErrorInExpr(pos) - | Self::ErrorDotExpr(_, pos) - | Self::ErrorTooManyOperations(pos) - | Self::ErrorTooManyModules(pos) - | Self::ErrorStackOverflow(pos) - | Self::ErrorTerminated(pos) => write!(f, "{} ({})", desc, pos), + Self::ErrorIndexingType(_, _) + | Self::ErrorNumericIndexExpr(_) + | Self::ErrorStringIndexExpr(_) + | Self::ErrorImportExpr(_) + | Self::ErrorLogicGuard(_) + | Self::ErrorFor(_) + | Self::ErrorAssignmentToUnknownLHS(_) + | Self::ErrorInExpr(_) + | Self::ErrorDotExpr(_, _) + | Self::ErrorTooManyOperations(_) + | Self::ErrorTooManyModules(_) + | Self::ErrorStackOverflow(_) + | Self::ErrorTerminated(_) => write!(f, "{}", desc)?, - Self::ErrorRuntime(s, pos) => { - write!(f, "{} ({})", if s.is_empty() { desc } else { s }, pos) + Self::ErrorRuntime(s, _) => write!(f, "{}", if s.is_empty() { desc } else { s })?, + + Self::ErrorAssignmentToConstant(s, _) => write!(f, "{}: '{}'", desc, s)?, + Self::ErrorMismatchOutputType(s, _) => write!(f, "{}: {}", desc, s)?, + Self::ErrorArithmetic(s, _) => write!(f, "{}", s)?, + + Self::ErrorLoopBreak(_, _) => write!(f, "{}", desc)?, + Self::Return(_, _) => write!(f, "{}", desc)?, + + Self::ErrorBooleanArgMismatch(op, _) => { + write!(f, "{} operator expects boolean operands", op)? } - - Self::ErrorAssignmentToConstant(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos), - Self::ErrorMismatchOutputType(s, pos) => write!(f, "{}: {} ({})", desc, s, pos), - Self::ErrorArithmetic(s, pos) => write!(f, "{} ({})", s, pos), - - Self::ErrorLoopBreak(_, pos) => write!(f, "{} ({})", desc, pos), - Self::Return(_, pos) => write!(f, "{} ({})", desc, pos), - - Self::ErrorBooleanArgMismatch(op, pos) => { - write!(f, "{} operator expects boolean operands ({})", op, pos) + Self::ErrorCharMismatch(_) => write!(f, "string indexing expects a character value")?, + Self::ErrorArrayBounds(_, index, _) if *index < 0 => { + write!(f, "{}: {} < 0", desc, index)? } - Self::ErrorCharMismatch(pos) => { - write!(f, "string indexing expects a character value ({})", pos) - } - Self::ErrorArrayBounds(_, index, pos) if *index < 0 => { - write!(f, "{}: {} < 0 ({})", desc, index, pos) - } - Self::ErrorArrayBounds(0, _, pos) => write!(f, "{} ({})", desc, pos), - Self::ErrorArrayBounds(1, index, pos) => write!( + Self::ErrorArrayBounds(0, _, _) => write!(f, "{}", desc)?, + Self::ErrorArrayBounds(1, index, _) => write!( f, - "Array index {} is out of bounds: only one element in the array ({})", - index, pos - ), - Self::ErrorArrayBounds(max, index, pos) => write!( + "Array index {} is out of bounds: only one element in the array", + index + )?, + Self::ErrorArrayBounds(max, index, _) => write!( f, - "Array index {} is out of bounds: only {} elements in the array ({})", - index, max, pos - ), - Self::ErrorStringBounds(_, index, pos) if *index < 0 => { - write!(f, "{}: {} < 0 ({})", desc, index, pos) + "Array index {} is out of bounds: only {} elements in the array", + index, max + )?, + Self::ErrorStringBounds(_, index, _) if *index < 0 => { + write!(f, "{}: {} < 0", desc, index)? } - Self::ErrorStringBounds(0, _, pos) => write!(f, "{} ({})", desc, pos), - Self::ErrorStringBounds(1, index, pos) => write!( + Self::ErrorStringBounds(0, _, _) => write!(f, "{}", desc)?, + Self::ErrorStringBounds(1, index, _) => write!( f, - "String index {} is out of bounds: only one character in the string ({})", - index, pos - ), - Self::ErrorStringBounds(max, index, pos) => write!( + "String index {} is out of bounds: only one character in the string", + index + )?, + Self::ErrorStringBounds(max, index, _) => write!( f, - "String index {} is out of bounds: only {} characters in the string ({})", - index, max, pos - ), + "String index {} is out of bounds: only {} characters in the string", + index, max + )?, + Self::ErrorDataTooLarge(typ, max, size, _) => { + write!(f, "{} ({}) exceeds the maximum limit ({})", typ, size, max)? + } } - } -} -impl From for Box { - fn from(err: ParseError) -> Self { - Box::new(EvalAltResult::ErrorParsing(err)) + // Do not write any position if None + if !pos.is_none() { + write!(f, " ({})", pos)?; + } + + Ok(()) } } @@ -259,11 +263,11 @@ impl EvalAltResult { pub fn position(&self) -> Position { match self { #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] Self::ErrorReadingScriptFile(_, pos, _) => *pos, - Self::ErrorParsing(err) => err.position(), - - Self::ErrorFunctionNotFound(_, pos) + Self::ErrorParsing(_, pos) + | Self::ErrorFunctionNotFound(_, pos) | Self::ErrorInFunctionCall(_, _, pos) | Self::ErrorBooleanArgMismatch(_, pos) | Self::ErrorCharMismatch(pos) @@ -286,6 +290,7 @@ impl EvalAltResult { | Self::ErrorTooManyOperations(pos) | Self::ErrorTooManyModules(pos) | Self::ErrorStackOverflow(pos) + | Self::ErrorDataTooLarge(_, _, _, pos) | Self::ErrorTerminated(pos) | Self::ErrorRuntime(_, pos) | Self::ErrorLoopBreak(_, pos) @@ -297,11 +302,11 @@ impl EvalAltResult { pub fn set_position(&mut self, new_position: Position) { match self { #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] Self::ErrorReadingScriptFile(_, pos, _) => *pos = new_position, - Self::ErrorParsing(err) => err.1 = new_position, - - Self::ErrorFunctionNotFound(_, pos) + Self::ErrorParsing(_, pos) + | Self::ErrorFunctionNotFound(_, pos) | Self::ErrorInFunctionCall(_, _, pos) | Self::ErrorBooleanArgMismatch(_, pos) | Self::ErrorCharMismatch(pos) @@ -324,6 +329,7 @@ impl EvalAltResult { | Self::ErrorTooManyOperations(pos) | Self::ErrorTooManyModules(pos) | Self::ErrorStackOverflow(pos) + | Self::ErrorDataTooLarge(_, _, _, pos) | Self::ErrorTerminated(pos) | Self::ErrorRuntime(_, pos) | Self::ErrorLoopBreak(_, pos) @@ -331,10 +337,12 @@ impl EvalAltResult { } } - /// Consume the current `EvalAltResult` and return a new one - /// with the specified `Position`. + /// Consume the current `EvalAltResult` and return a new one with the specified `Position` + /// if the current position is `Position::None`. pub(crate) fn new_position(mut self: Box, new_position: Position) -> Box { - self.set_position(new_position); + if self.position().is_none() { + self.set_position(new_position); + } self } } diff --git a/src/scope.rs b/src/scope.rs index 1b9c8b1d..97d3770e 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,12 +1,10 @@ //! Module that defines the `Scope` type representing a function call-stack scope. use crate::any::{Dynamic, Union, Variant}; +use crate::module::Module; use crate::parser::{map_dynamic_to_expr, Expr}; use crate::token::Position; -#[cfg(not(feature = "no_module"))] -use crate::module::Module; - use crate::stdlib::{borrow::Cow, boxed::Box, iter, string::String, vec::Vec}; /// Type of an entry in the Scope. @@ -177,7 +175,18 @@ impl<'a> Scope<'a> { /// /// Modules are used for accessing member variables, functions and plugins under a namespace. #[cfg(not(feature = "no_module"))] - pub fn push_module>>(&mut self, name: K, mut value: Module) { + pub fn push_module>>(&mut self, name: K, value: Module) { + self.push_module_internal(name, value); + } + + /// Add (push) a new module to the Scope. + /// + /// Modules are used for accessing member variables, functions and plugins under a namespace. + pub(crate) fn push_module_internal>>( + &mut self, + name: K, + mut value: Module, + ) { value.index_all_sub_modules(); self.push_dynamic_value( @@ -350,6 +359,11 @@ impl<'a> Scope<'a> { /// Find a module in the Scope, starting from the last entry. #[cfg(not(feature = "no_module"))] pub fn find_module(&mut self, name: &str) -> Option<&mut Module> { + self.find_module_internal(name) + } + + /// Find a module in the Scope, starting from the last entry. + pub(crate) fn find_module_internal(&mut self, name: &str) -> Option<&mut Module> { let index = self.get_module_index(name)?; self.get_mut(index).0.downcast_mut::() } @@ -403,7 +417,7 @@ impl<'a> Scope<'a> { pub fn set_value(&mut self, name: &'a str, value: T) { match self.get_index(name) { None => self.push(name, value), - Some((_, EntryType::Constant)) => panic!("variable {} is constant", name), + Some((_, EntryType::Constant)) => unreachable!("variable {} is constant", name), Some((index, EntryType::Normal)) => { self.0.get_mut(index).unwrap().value = Dynamic::from(value) } diff --git a/src/stdlib.rs b/src/stdlib.rs index 1d1397d5..64eda6fb 100644 --- a/src/stdlib.rs +++ b/src/stdlib.rs @@ -4,10 +4,13 @@ mod inner { pub use core::{ any, arch, array, ascii, cell, char, clone, cmp, convert, default, f32, f64, ffi, fmt, - future, hash, hint, i128, i16, i32, i64, i8, isize, iter, marker, mem, num, ops, option, - panic, pin, prelude, ptr, result, slice, str, task, time, u128, u16, u32, u64, u8, usize, + future, hash, hint, i16, i32, i64, i8, isize, iter, marker, mem, num, ops, option, panic, + pin, prelude, ptr, result, slice, str, task, time, u16, u32, u64, u8, usize, }; + #[cfg(not(target_arch = "wasm32"))] + pub use core::{i128, u128}; + pub use alloc::{borrow, boxed, format, rc, string, sync, vec}; pub use core_error as error; diff --git a/src/token.rs b/src/token.rs index dda005a9..d5f84902 100644 --- a/src/token.rs +++ b/src/token.rs @@ -19,6 +19,8 @@ use crate::stdlib::{ type LERR = LexError; +pub type TokenStream<'a> = Peekable>; + /// A location (line number + character position) in the input script. /// /// In order to keep footprint small, both line number and character position have 16-bit unsigned resolution, @@ -181,6 +183,7 @@ pub enum Token { XOr, Ampersand, And, + #[cfg(not(feature = "no_function"))] Fn, Continue, Break, @@ -197,8 +200,10 @@ pub enum Token { XOrAssign, ModuloAssign, PowerOfAssign, + #[cfg(not(feature = "no_function"))] Private, Import, + #[cfg(not(feature = "no_module"))] Export, As, LexError(Box), @@ -260,6 +265,7 @@ impl Token { Or => "||", Ampersand => "&", And => "&&", + #[cfg(not(feature = "no_function"))] Fn => "fn", Continue => "continue", Break => "break", @@ -281,12 +287,14 @@ impl Token { ModuloAssign => "%=", PowerOf => "~", PowerOfAssign => "~=", + #[cfg(not(feature = "no_function"))] Private => "private", Import => "import", + #[cfg(not(feature = "no_module"))] Export => "export", As => "as", EOF => "{EOF}", - _ => panic!("operator should be match in outer scope"), + _ => unreachable!("operator should be match in outer scope"), }) .into(), } @@ -421,6 +429,8 @@ impl From for String { /// An iterator on a `Token` stream. pub struct TokenIterator<'a> { + /// Maximum length of a string (0 = unlimited). + max_string_size: usize, /// Can the next token be a unary operator? can_be_unary: bool, /// Current position. @@ -486,6 +496,7 @@ impl<'a> TokenIterator<'a> { pub fn parse_string_literal( &mut self, enclosing_char: char, + max_length: usize, ) -> Result { let mut result = Vec::new(); let mut escape = String::with_capacity(12); @@ -497,6 +508,10 @@ impl<'a> TokenIterator<'a> { self.advance(); + if max_length > 0 && result.len() > max_length { + return Err((LexError::StringTooLong(max_length), self.pos)); + } + match next_char { // \... '\\' if escape.is_empty() => { @@ -533,7 +548,7 @@ impl<'a> TokenIterator<'a> { 'x' => 2, 'u' => 4, 'U' => 8, - _ => panic!("should be 'x', 'u' or 'U'"), + _ => unreachable!(), }; for _ in 0..len { @@ -584,7 +599,13 @@ impl<'a> TokenIterator<'a> { } } - Ok(result.iter().collect()) + let s = result.iter().collect::(); + + if max_length > 0 && s.len() > max_length { + return Err((LexError::StringTooLong(max_length), self.pos)); + } + + Ok(s) } /// Get the next token. @@ -646,14 +667,14 @@ impl<'a> TokenIterator<'a> { '0', '1', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', ], - _ => panic!("unexpected character {}", ch), + _ => unreachable!(), }; radix_base = Some(match ch { 'x' | 'X' => 16, 'o' | 'O' => 8, 'b' | 'B' => 2, - _ => panic!("unexpected character {}", ch), + _ => unreachable!(), }); while let Some(next_char_in_hex) = self.peek_next() { @@ -753,13 +774,11 @@ impl<'a> TokenIterator<'a> { "throw" => Token::Throw, "for" => Token::For, "in" => Token::In, + #[cfg(not(feature = "no_function"))] "private" => Token::Private, - - #[cfg(not(feature = "no_module"))] "import" => Token::Import, #[cfg(not(feature = "no_module"))] "export" => Token::Export, - #[cfg(not(feature = "no_module"))] "as" => Token::As, #[cfg(not(feature = "no_function"))] @@ -773,10 +792,12 @@ impl<'a> TokenIterator<'a> { // " - string literal ('"', _) => { - return self.parse_string_literal('"').map_or_else( - |err| Some((Token::LexError(Box::new(err.0)), err.1)), - |out| Some((Token::StringConst(out), pos)), - ); + return self + .parse_string_literal('"', self.max_string_size) + .map_or_else( + |err| Some((Token::LexError(Box::new(err.0)), err.1)), + |out| Some((Token::StringConst(out), pos)), + ); } // ' - character literal @@ -787,19 +808,25 @@ impl<'a> TokenIterator<'a> { )); } ('\'', _) => { - return Some(self.parse_string_literal('\'').map_or_else( - |err| (Token::LexError(Box::new(err.0)), err.1), - |result| { - let mut chars = result.chars(); - let first = chars.next(); + return Some( + self.parse_string_literal('\'', self.max_string_size) + .map_or_else( + |err| (Token::LexError(Box::new(err.0)), err.1), + |result| { + let mut chars = result.chars(); + let first = chars.next(); - if chars.next().is_some() { - (Token::LexError(Box::new(LERR::MalformedChar(result))), pos) - } else { - (Token::CharConstant(first.expect("should be Some")), pos) - } - }, - )); + if chars.next().is_some() { + ( + Token::LexError(Box::new(LERR::MalformedChar(result))), + pos, + ) + } else { + (Token::CharConstant(first.expect("should be Some")), pos) + } + }, + ), + ); } // Braces @@ -835,6 +862,14 @@ impl<'a> TokenIterator<'a> { self.eat_next(); return Some((Token::MinusAssign, pos)); } + ('-', '>') => { + return Some(( + Token::LexError(Box::new(LERR::ImproperSymbol( + "'->' is not a valid symbol. This is not C or C++!".to_string(), + ))), + pos, + )) + } ('-', _) if self.can_be_unary => return Some((Token::UnaryMinus, pos)), ('-', _) => return Some((Token::Minus, pos)), @@ -904,7 +939,7 @@ impl<'a> TokenIterator<'a> { // Warn against `===` if self.peek_next() == Some('=') { return Some(( - Token::LexError(Box::new(LERR::ImproperKeyword( + Token::LexError(Box::new(LERR::ImproperSymbol( "'===' is not a valid operator. This is not JavaScript! Should it be '=='?" .to_string(), ))), @@ -914,19 +949,44 @@ impl<'a> TokenIterator<'a> { return Some((Token::EqualsTo, pos)); } + ('=', '>') => { + return Some(( + Token::LexError(Box::new(LERR::ImproperSymbol( + "'=>' is not a valid symbol. This is not Rust! Should it be '>='?" + .to_string(), + ))), + pos, + )) + } ('=', _) => return Some((Token::Equals, pos)), - #[cfg(not(feature = "no_module"))] (':', ':') => { self.eat_next(); return Some((Token::DoubleColon, pos)); } + (':', '=') => { + return Some(( + Token::LexError(Box::new(LERR::ImproperSymbol( + "':=' is not a valid assignment operator. This is not Pascal! Should it be simply '='?" + .to_string(), + ))), + pos, + )) + } (':', _) => return Some((Token::Colon, pos)), ('<', '=') => { self.eat_next(); return Some((Token::LessThanEqualsTo, pos)); } + ('<', '-') => { + return Some(( + Token::LexError(Box::new(LERR::ImproperSymbol( + "'<-' is not a valid symbol. Should it be '<='?".to_string(), + ))), + pos, + )) + } ('<', '<') => { self.eat_next(); @@ -967,7 +1027,7 @@ impl<'a> TokenIterator<'a> { // Warn against `!==` if self.peek_next() == Some('=') { return Some(( - Token::LexError(Box::new(LERR::ImproperKeyword( + Token::LexError(Box::new(LERR::ImproperSymbol( "'!==' is not a valid operator. This is not JavaScript! Should it be '!='?" .to_string(), ))), @@ -1017,7 +1077,7 @@ impl<'a> TokenIterator<'a> { } ('~', _) => return Some((Token::PowerOf, pos)), - ('\0', _) => panic!("should not be EOF"), + ('\0', _) => unreachable!(), (ch, _) if ch.is_whitespace() => (), (ch, _) => return Some((Token::LexError(Box::new(LERR::UnexpectedChar(ch))), pos)), @@ -1042,8 +1102,9 @@ impl<'a> Iterator for TokenIterator<'a> { } /// Tokenize an input text stream. -pub fn lex<'a>(input: &'a [&'a str]) -> TokenIterator<'a> { +pub fn lex<'a>(input: &'a [&'a str], max_string_size: usize) -> TokenIterator<'a> { TokenIterator { + max_string_size, can_be_unary: true, pos: Position::new(1, 0), streams: input.iter().map(|s| s.chars().peekable()).collect(), diff --git a/src/utils.rs b/src/utils.rs index ccd3d6be..77cef0e7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -190,8 +190,9 @@ impl Clone for StaticVec { if self.is_fixed_storage() { for x in 0..self.len { - let item: &T = unsafe { mem::transmute(self.list.get(x).unwrap()) }; - value.list[x] = MaybeUninit::new(item.clone()); + let item = self.list.get(x).unwrap(); + let item_value = unsafe { mem::transmute::<_, &T>(item) }; + value.list[x] = MaybeUninit::new(item_value.clone()); } } else { value.more = self.more.clone(); @@ -215,6 +216,15 @@ impl FromIterator for StaticVec { } } +impl IntoIterator for StaticVec { + type Item = T; + type IntoIter = Box>; + + fn into_iter(self) -> Self::IntoIter { + self.into_iter() + } +} + impl StaticVec { /// Create a new `StaticVec`. #[inline(always)] @@ -424,7 +434,7 @@ impl StaticVec { panic!("index OOB in StaticVec"); } - let list: &[T; MAX_STATIC_VEC] = unsafe { mem::transmute(&self.list) }; + let list = unsafe { mem::transmute::<_, &[T; MAX_STATIC_VEC]>(&self.list) }; if self.is_fixed_storage() { list.get(index).unwrap() @@ -442,7 +452,7 @@ impl StaticVec { panic!("index OOB in StaticVec"); } - let list: &mut [T; MAX_STATIC_VEC] = unsafe { mem::transmute(&mut self.list) }; + let list = unsafe { mem::transmute::<_, &mut [T; MAX_STATIC_VEC]>(&mut self.list) }; if self.is_fixed_storage() { list.get_mut(index).unwrap() @@ -452,7 +462,7 @@ impl StaticVec { } /// Get an iterator to entries in the `StaticVec`. pub fn iter(&self) -> impl Iterator { - let list: &[T; MAX_STATIC_VEC] = unsafe { mem::transmute(&self.list) }; + let list = unsafe { mem::transmute::<_, &[T; MAX_STATIC_VEC]>(&self.list) }; if self.is_fixed_storage() { list[..self.len].iter() @@ -462,7 +472,7 @@ impl StaticVec { } /// Get a mutable iterator to entries in the `StaticVec`. pub fn iter_mut(&mut self) -> impl Iterator { - let list: &mut [T; MAX_STATIC_VEC] = unsafe { mem::transmute(&mut self.list) }; + let list = unsafe { mem::transmute::<_, &mut [T; MAX_STATIC_VEC]>(&mut self.list) }; if self.is_fixed_storage() { list[..self.len].iter_mut() @@ -541,15 +551,13 @@ impl StaticVec { impl fmt::Debug for StaticVec { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "[ ")?; - self.iter().try_for_each(|v| write!(f, "{:?}, ", v))?; - write!(f, "]") + fmt::Debug::fmt(&self.iter().collect::>(), f) } } impl AsRef<[T]> for StaticVec { fn as_ref(&self) -> &[T] { - let list: &[T; MAX_STATIC_VEC] = unsafe { mem::transmute(&self.list) }; + let list = unsafe { mem::transmute::<_, &[T; MAX_STATIC_VEC]>(&self.list) }; if self.is_fixed_storage() { &list[..self.len] @@ -561,7 +569,7 @@ impl AsRef<[T]> for StaticVec { impl AsMut<[T]> for StaticVec { fn as_mut(&mut self) -> &mut [T] { - let list: &mut [T; MAX_STATIC_VEC] = unsafe { mem::transmute(&mut self.list) }; + let list = unsafe { mem::transmute::<_, &mut [T; MAX_STATIC_VEC]>(&mut self.list) }; if self.is_fixed_storage() { &mut list[..self.len] diff --git a/tests/arrays.rs b/tests/arrays.rs index 741156e9..8ea61cdf 100644 --- a/tests/arrays.rs +++ b/tests/arrays.rs @@ -6,6 +6,7 @@ fn test_arrays() -> Result<(), Box> { let engine = Engine::new(); assert_eq!(engine.eval::("let x = [1, 2, 3]; x[1]")?, 2); + assert_eq!(engine.eval::("let x = [1, 2, 3,]; x[1]")?, 2); assert_eq!(engine.eval::("let y = [1, 2, 3]; y[1] = 5; y[1]")?, 5); assert_eq!( engine.eval::(r#"let y = [1, [ 42, 88, "93" ], 3]; y[1][2][1]"#)?, diff --git a/tests/call_fn.rs b/tests/call_fn.rs index e3c61765..dd46450d 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -1,19 +1,17 @@ #![cfg(not(feature = "no_function"))] -use rhai::{Engine, EvalAltResult, Func, ParseErrorType, Scope, INT}; +use rhai::{Engine, EvalAltResult, Func, ParseError, ParseErrorType, Scope, INT}; #[test] fn test_fn() -> Result<(), Box> { let engine = Engine::new(); // Expect duplicated parameters error - match engine - .compile("fn hello(x, x) { x }") - .expect_err("should be error") - .error_type() - { - ParseErrorType::FnDuplicatedParam(f, p) if f == "hello" && p == "x" => (), - _ => assert!(false, "wrong error"), - } + assert!(matches!( + engine + .compile("fn hello(x, x) { x }") + .expect_err("should be error"), + ParseError(x, _) if *x == ParseErrorType::FnDuplicatedParam("hello".to_string(), "x".to_string()) + )); Ok(()) } @@ -70,7 +68,7 @@ fn test_call_fn_private() -> Result<(), Box> { let r: INT = engine.call_fn(&mut scope, &ast, "add", (40 as INT, 2 as INT))?; assert_eq!(r, 42); - let ast = engine.compile("private fn add(x, n) { x + n }")?; + let ast = engine.compile("private fn add(x, n, ) { x + n }")?; assert!(matches!( *engine.call_fn::<_, INT>(&mut scope, &ast, "add", (40 as INT, 2 as INT)) @@ -85,12 +83,20 @@ fn test_call_fn_private() -> Result<(), Box> { fn test_anonymous_fn() -> Result<(), Box> { let calc_func = Func::<(INT, INT, INT), INT>::create_from_script( Engine::new(), - "fn calc(x, y, z) { (x + y) * z }", + "fn calc(x, y, z,) { (x + y) * z }", "calc", )?; assert_eq!(calc_func(42, 123, 9)?, 1485); + let calc_func = Func::<(INT, String, INT), INT>::create_from_script( + Engine::new(), + "fn calc(x, y, z) { (x + len(y)) * z }", + "calc", + )?; + + assert_eq!(calc_func(42, "hello".to_string(), 9)?, 423); + let calc_func = Func::<(INT, INT, INT), INT>::create_from_script( Engine::new(), "private fn calc(x, y, z) { (x + y) * z }", diff --git a/tests/constants.rs b/tests/constants.rs index d5e0820a..266b6ac2 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -7,14 +7,16 @@ fn test_constant() -> Result<(), Box> { assert_eq!(engine.eval::("const x = 123; x")?, 123); assert!(matches!( - *engine.eval::("const x = 123; x = 42;").expect_err("expects error"), - EvalAltResult::ErrorParsing(err) if err.error_type() == &ParseErrorType::AssignmentToConstant("x".to_string()) + *engine + .eval::("const x = 123; x = 42;") + .expect_err("expects error"), + EvalAltResult::ErrorParsing(ParseErrorType::AssignmentToConstant(x), _) if x == "x" )); #[cfg(not(feature = "no_index"))] assert!(matches!( *engine.eval::("const x = [1, 2, 3, 4, 5]; x[2] = 42;").expect_err("expects error"), - EvalAltResult::ErrorParsing(err) if err.error_type() == &ParseErrorType::AssignmentToConstant("x".to_string()) + EvalAltResult::ErrorParsing(ParseErrorType::AssignmentToConstant(x), _) if x == "x" )); Ok(()) diff --git a/tests/data_size.rs b/tests/data_size.rs new file mode 100644 index 00000000..e073d443 --- /dev/null +++ b/tests/data_size.rs @@ -0,0 +1,259 @@ +#![cfg(not(feature = "unchecked"))] +use rhai::{Engine, EvalAltResult, ParseError, ParseErrorType}; + +#[cfg(not(feature = "no_index"))] +use rhai::Array; + +#[cfg(not(feature = "no_object"))] +use rhai::Map; + +#[test] +fn test_max_string_size() -> Result<(), Box> { + let mut engine = Engine::new(); + engine.set_max_string_size(10); + + assert!(matches!( + engine.compile(r#"let x = "hello, world!";"#).expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::LiteralTooLarge("Length of string literal".to_string(), 10) + )); + + assert!(matches!( + engine.compile(r#"let x = "朝に紅顔、暮に白骨";"#).expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::LiteralTooLarge("Length of string literal".to_string(), 10) + )); + + assert!(matches!( + *engine + .eval::( + r#" + let x = "hello, "; + let y = "world!"; + x + y + "# + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 13, _) + )); + + assert!(matches!( + *engine + .eval::( + r#" + let x = "hello"; + x.pad(100, '!'); + x + "# + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 100, _) + )); + + engine.set_max_string_size(0); + + assert_eq!( + engine.eval::( + r#" + let x = "hello, "; + let y = "world!"; + x + y + "# + )?, + "hello, world!" + ); + + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_index"))] +fn test_max_array_size() -> Result<(), Box> { + let mut engine = Engine::new(); + engine.set_max_array_size(10); + + #[cfg(not(feature = "no_object"))] + engine.set_max_map_size(10); + + assert!(matches!( + engine + .compile("let x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];") + .expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::LiteralTooLarge("Size of array literal".to_string(), 10) + )); + + assert!(matches!( + *engine + .eval::( + r" + let x = [1,2,3,4,5,6]; + let y = [7,8,9,10,11,12]; + x + y + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) + )); + assert!(matches!( + *engine + .eval::( + r" + let x = [1,2,3,4,5,6]; + x.pad(100, 42); + x + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 100, _) + )); + + assert!(matches!( + *engine + .eval::( + r" + let x = [1,2,3]; + [x, x, x, x] + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) + )); + + #[cfg(not(feature = "no_object"))] + assert!(matches!( + *engine + .eval::( + r" + let x = #{a:1, b:2, c:3}; + [x, x, x, x] + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) + )); + + assert!(matches!( + *engine + .eval::( + r" + let x = [1]; + let y = [x, x]; + let z = [y, y]; + [z, z, z] + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) + )); + + engine.set_max_array_size(0); + + assert_eq!( + engine + .eval::( + r" + let x = [1,2,3,4,5,6]; + let y = [7,8,9,10,11,12]; + x + y + " + )? + .len(), + 12 + ); + + assert_eq!( + engine + .eval::( + r" + let x = [1,2,3]; + [x, x, x, x] + " + )? + .len(), + 4 + ); + + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_object"))] +fn test_max_map_size() -> Result<(), Box> { + let mut engine = Engine::new(); + engine.set_max_map_size(10); + + #[cfg(not(feature = "no_index"))] + engine.set_max_array_size(10); + + assert!(matches!( + engine + .compile("let x = #{a:1,b:2,c:3,d:4,e:5,f:6,g:7,h:8,i:9,j:10,k:11,l:12,m:13,n:14,o:15};") + .expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::LiteralTooLarge("Number of properties in object map literal".to_string(), 10) + )); + + assert!(matches!( + *engine + .eval::( + r" + let x = #{a:1,b:2,c:3,d:4,e:5,f:6}; + let y = #{g:7,h:8,i:9,j:10,k:11,l:12}; + x + y + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) + )); + + assert!(matches!( + *engine + .eval::( + r" + let x = #{a:1,b:2,c:3}; + #{u:x, v:x, w:x, z:x} + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) + )); + + #[cfg(not(feature = "no_index"))] + assert!(matches!( + *engine + .eval::( + r" + let x = [1, 2, 3]; + #{u:x, v:x, w:x, z:x} + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) + )); + + engine.set_max_map_size(0); + + assert_eq!( + engine + .eval::( + r" + let x = #{a:1,b:2,c:3,d:4,e:5,f:6}; + let y = #{g:7,h:8,i:9,j:10,k:11,l:12}; + x + y + " + )? + .len(), + 12 + ); + + assert_eq!( + engine + .eval::( + r" + let x = #{a:1,b:2,c:3}; + #{u:x, v:x, w:x, z:x} + " + )? + .len(), + 4 + ); + + Ok(()) +} diff --git a/tests/decrement.rs b/tests/decrement.rs index ab1f9376..1b74dfe9 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 == "- (string, string)" + EvalAltResult::ErrorFunctionNotFound(err, _) if err == "- (&str | ImmutableString, &str | ImmutableString)" )); Ok(()) diff --git a/tests/expressions.rs b/tests/expressions.rs index 35a6b90c..771d1d02 100644 --- a/tests/expressions.rs +++ b/tests/expressions.rs @@ -12,16 +12,17 @@ fn test_expressions() -> Result<(), Box> { engine.eval_expression_with_scope::(&mut scope, "2 + (x + 10) * 2")?, 42 ); - assert_eq!( - engine.eval_expression_with_scope::(&mut scope, "if x > 0 { 42 } else { 123 }")?, - 42 - ); + assert!(engine + .eval_expression_with_scope::(&mut scope, "if x > 0 { 42 } else { 123 }") + .is_err()); assert!(engine.eval_expression::<()>("40 + 2;").is_err()); assert!(engine.eval_expression::<()>("40 + { 2 }").is_err()); assert!(engine.eval_expression::<()>("x = 42").is_err()); assert!(engine.compile_expression("let x = 42").is_err()); + engine.compile("40 + { let x = 2; x }")?; + Ok(()) } diff --git a/tests/for.rs b/tests/for.rs index cddff6ed..4f99c06f 100644 --- a/tests/for.rs +++ b/tests/for.rs @@ -30,6 +30,26 @@ fn test_for_array() -> Result<(), Box> { Ok(()) } +#[test] +fn test_for_string() -> Result<(), Box> { + let engine = Engine::new(); + + let script = r#" + let s = "hello"; + let sum = 0; + + for ch in s { + sum += ch.to_int(); + } + + sum + "#; + + assert_eq!(engine.eval::(script)?, 532); + + Ok(()) +} + #[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_index"))] #[test] diff --git a/tests/functions.rs b/tests/functions.rs index ae12e052..6a94256a 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -7,6 +7,11 @@ fn test_functions() -> Result<(), Box> { assert_eq!(engine.eval::("fn add(x, n) { x + n } add(40, 2)")?, 42); + assert_eq!( + engine.eval::("fn add(x, n,) { x + n } add(40, 2,)")?, + 42 + ); + assert_eq!( engine.eval::("fn add(x, n) { x + n } let a = 40; add(a, 2); a")?, 40 diff --git a/tests/get_set.rs b/tests/get_set.rs index 2df59671..ca5c0c47 100644 --- a/tests/get_set.rs +++ b/tests/get_set.rs @@ -42,18 +42,25 @@ fn test_get_set() -> Result<(), Box> { engine.register_fn("add", |value: &mut INT| *value += 41); engine.register_fn("new_ts", TestStruct::new); - #[cfg(not(feature = "no_index"))] - engine.register_indexer(|value: &mut TestStruct, index: ImmutableString| { - value.array[index.len()] - }); - assert_eq!(engine.eval::("let a = new_ts(); a.x = 500; a.x")?, 500); assert_eq!(engine.eval::("let a = new_ts(); a.x.add(); a.x")?, 42); assert_eq!(engine.eval::("let a = new_ts(); a.y.add(); a.y")?, 0); #[cfg(not(feature = "no_index"))] - assert_eq!(engine.eval::(r#"let a = new_ts(); a["abc"]"#)?, 4); + { + engine.register_indexer_get_set( + |value: &mut TestStruct, index: ImmutableString| value.array[index.len()], + |value: &mut TestStruct, index: ImmutableString, new_val: INT| { + value.array[index.len()] = new_val + }, + ); + assert_eq!(engine.eval::(r#"let a = new_ts(); a["abc"]"#)?, 4); + assert_eq!( + engine.eval::(r#"let a = new_ts(); a["abc"] = 42; a["abc"]"#)?, + 42 + ); + } Ok(()) } diff --git a/tests/internal_fn.rs b/tests/internal_fn.rs index 764cf601..67417344 100644 --- a/tests/internal_fn.rs +++ b/tests/internal_fn.rs @@ -10,6 +10,12 @@ fn test_internal_fn() -> Result<(), Box> { engine.eval::("fn add_me(a, b) { a+b } add_me(3, 4)")?, 7 ); + + assert_eq!( + engine.eval::("fn add_me(a, b,) { a+b } add_me(3, 4,)")?, + 7 + ); + assert_eq!(engine.eval::("fn bob() { return 4; 5 } bob()")?, 4); Ok(()) diff --git a/tests/looping.rs b/tests/looping.rs index a973c98f..3a4804ce 100644 --- a/tests/looping.rs +++ b/tests/looping.rs @@ -1,4 +1,4 @@ -use rhai::{Engine, EvalAltResult, INT}; +use rhai::{Engine, EvalAltResult, ParseError, ParseErrorType, INT}; #[test] fn test_loop() -> Result<(), Box> { @@ -26,5 +26,15 @@ fn test_loop() -> Result<(), Box> { 21 ); + assert!(matches!( + engine.compile("let x = 0; break;").expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::LoopBreak + )); + + assert!(matches!( + engine.compile("let x = 0; if x > 0 { continue; }").expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::LoopBreak + )); + Ok(()) } diff --git a/tests/maps.rs b/tests/maps.rs index a7aef8dd..8061f1ed 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -12,6 +12,10 @@ fn test_map_indexing() -> Result<(), Box> { engine.eval::(r#"let x = #{a: 1, b: 2, c: 3}; x["b"]"#)?, 2 ); + assert_eq!( + engine.eval::(r#"let x = #{a: 1, b: 2, c: 3,}; x["b"]"#)?, + 2 + ); assert_eq!( engine.eval::( r#" diff --git a/tests/modules.rs b/tests/modules.rs index 61273f53..771aa490 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -1,5 +1,9 @@ #![cfg(not(feature = "no_module"))] -use rhai::{module_resolvers, Engine, EvalAltResult, Module, Scope, INT}; +use rhai::{ + module_resolvers, Dynamic, Engine, EvalAltResult, Module, ParseError, ParseErrorType, Scope, + INT, +}; +use std::any::TypeId; #[test] fn test_module() { @@ -18,6 +22,7 @@ fn test_module_sub_module() -> Result<(), Box> { let mut sub_module2 = Module::new(); sub_module2.set_var("answer", 41 as INT); + let hash_inc = sub_module2.set_fn_1("inc", |x: INT| Ok(x + 1)); sub_module.set_sub_module("universe", sub_module2); @@ -128,7 +133,7 @@ fn test_module_resolver() -> Result<(), Box> { EvalAltResult::ErrorInFunctionCall(fn_name, _, _) if fn_name == "foo" )); - engine.set_max_modules(0); + engine.set_max_modules(1000); #[cfg(not(feature = "no_function"))] engine.eval::<()>( @@ -195,6 +200,7 @@ fn test_module_from_ast() -> Result<(), Box> { let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?; let mut scope = Scope::new(); + scope.push_module("testing", module); assert_eq!( @@ -230,3 +236,20 @@ fn test_module_from_ast() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_module_export() -> Result<(), Box> { + let engine = Engine::new(); + + assert!(matches!( + engine.compile(r"let x = 10; { export x; }").expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::WrongExport + )); + + assert!(matches!( + engine.compile(r"fn abc(x) { export x; }").expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::WrongExport + )); + + Ok(()) +} diff --git a/tests/operations.rs b/tests/operations.rs index c02e2381..dae539f0 100644 --- a/tests/operations.rs +++ b/tests/operations.rs @@ -6,7 +6,7 @@ fn test_max_operations() -> Result<(), Box> { let mut engine = Engine::new(); engine.set_max_operations(500); - engine.on_progress(|count| { + engine.on_progress(|&count| { if count % 100 == 0 { println!("{}", count); } @@ -34,7 +34,7 @@ fn test_max_operations_functions() -> Result<(), Box> { let mut engine = Engine::new(); engine.set_max_operations(500); - engine.on_progress(|count| { + engine.on_progress(|&count| { if count % 100 == 0 { println!("{}", count); } @@ -90,7 +90,7 @@ fn test_max_operations_eval() -> Result<(), Box> { let mut engine = Engine::new(); engine.set_max_operations(500); - engine.on_progress(|count| { + engine.on_progress(|&count| { if count % 100 == 0 { println!("{}", count); } @@ -111,3 +111,20 @@ fn test_max_operations_eval() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_max_operations_progress() -> Result<(), Box> { + let mut engine = Engine::new(); + engine.set_max_operations(500); + + engine.on_progress(|&count| count < 100); + + assert!(matches!( + *engine + .eval::<()>("for x in range(0, 500) {}") + .expect_err("should error"), + EvalAltResult::ErrorTerminated(_) + )); + + Ok(()) +} diff --git a/tests/optimizer.rs b/tests/optimizer.rs index 49b15c25..828b0da4 100644 --- a/tests/optimizer.rs +++ b/tests/optimizer.rs @@ -3,7 +3,7 @@ use rhai::{Engine, EvalAltResult, OptimizationLevel, INT}; #[test] -fn test_optimizer() -> Result<(), Box> { +fn test_optimizer_run() -> Result<(), Box> { fn run_test(engine: &mut Engine) -> Result<(), Box> { assert_eq!(engine.eval::(r"if true { 42 } else { 123 }")?, 42); assert_eq!( @@ -30,3 +30,27 @@ fn test_optimizer() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_optimizer_parse() -> Result<(), Box> { + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::Simple); + + let ast = engine.compile("{ const DECISION = false; if DECISION { 42 } }")?; + + assert_eq!( + format!("{:?}", ast), + "AST([], )" + ); + + engine.set_optimization_level(OptimizationLevel::Full); + + let ast = engine.compile("if 1 == 2 { 42 }")?; + + assert_eq!( + format!("{:?}", ast), + "AST([], )" + ); + + Ok(()) +} diff --git a/tests/stack.rs b/tests/stack.rs index 1ba72e6a..29828fef 100644 --- a/tests/stack.rs +++ b/tests/stack.rs @@ -1,5 +1,5 @@ #![cfg(not(feature = "unchecked"))] -use rhai::{Engine, EvalAltResult, ParseErrorType}; +use rhai::{Engine, EvalAltResult, ParseError, ParseErrorType}; #[test] #[cfg(not(feature = "no_function"))] @@ -37,7 +37,7 @@ fn test_stack_overflow_parsing() -> Result<(), Box> { engine.compile(r" let a = (1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+1)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) ").expect_err("should error"), - err if err.error_type() == &ParseErrorType::ExprTooDeep + ParseError(x, _) if *x == ParseErrorType::ExprTooDeep )); engine.set_max_expr_depths(100, 6); @@ -71,7 +71,7 @@ fn test_stack_overflow_parsing() -> Result<(), Box> { 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 ").expect_err("should error"), - err if err.error_type() == &ParseErrorType::ExprTooDeep + ParseError(x, _) if *x == ParseErrorType::ExprTooDeep )); #[cfg(not(feature = "no_function"))] diff --git a/tests/string.rs b/tests/string.rs index b868f050..e6b77841 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -1,4 +1,4 @@ -use rhai::{Engine, EvalAltResult, INT}; +use rhai::{Engine, EvalAltResult, ImmutableString, RegisterFn, INT}; #[test] fn test_string() -> Result<(), Box> { @@ -23,9 +23,24 @@ fn test_string() -> Result<(), Box> { assert_eq!(engine.eval::(r#""foo" + 123"#)?, "foo123"); #[cfg(not(feature = "no_object"))] - assert_eq!(engine.eval::("(42).to_string()")?, "42"); + assert_eq!(engine.eval::("to_string(42)")?, "42"); + + #[cfg(not(feature = "no_index"))] + assert_eq!(engine.eval::(r#"let y = "hello"; y[1]"#)?, 'e'); #[cfg(not(feature = "no_object"))] + assert_eq!(engine.eval::(r#"let y = "hello"; y.len"#)?, 5); + + #[cfg(not(feature = "no_object"))] + assert_eq!( + engine.eval::(r#"let y = "hello"; y.clear(); y.len"#)?, + 0 + ); + + assert_eq!(engine.eval::(r#"let y = "hello"; len(y)"#)?, 5); + + #[cfg(not(feature = "no_object"))] + #[cfg(not(feature = "no_index"))] assert_eq!(engine.eval::(r#"let y = "hello"; y[y.len-1]"#)?, 'o'); #[cfg(not(feature = "no_float"))] @@ -139,3 +154,36 @@ fn test_string_substring() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_string_fn() -> Result<(), Box> { + let mut engine = Engine::new(); + + engine.register_fn("set_to_x", |ch: &mut char| *ch = 'X'); + + #[cfg(not(feature = "no_index"))] + #[cfg(not(feature = "no_object"))] + assert_eq!( + engine.eval::(r#"let x="foo"; x[0].set_to_x(); x"#)?, + "Xoo" + ); + #[cfg(not(feature = "no_index"))] + assert_eq!( + engine.eval::(r#"let x="foo"; set_to_x(x[0]); x"#)?, + "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); + + 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)" + )); + + Ok(()) +} diff --git a/tests/time.rs b/tests/time.rs index 2590e174..9873decf 100644 --- a/tests/time.rs +++ b/tests/time.rs @@ -1,4 +1,5 @@ #![cfg(not(feature = "no_std"))] +#![cfg(not(target_arch = "wasm32"))] use rhai::{Engine, EvalAltResult, INT};