diff --git a/Cargo.toml b/Cargo.toml index 97512851..2ffbd617 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rhai" -version = "0.15.2" +version = "0.16.0" edition = "2018" authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung"] description = "Embedded scripting for Rust" @@ -33,6 +33,7 @@ no_index = [] # no arrays and indexing no_object = [] # no custom objects no_function = [] # no script-defined functions no_module = [] # no modules +internals = [] # expose internal data structures # compiling for no-std no_std = [ "num-traits/libm", "hashbrown", "core-error", "libm", "ahash" ] diff --git a/README.md b/README.md index b4f0853d..952d9bf3 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Supported targets and builds Features -------- -* Easy-to-use language similar to JS+Rust with dynamic typing. +* Easy-to-use language similar to JavaScript+Rust with dynamic typing. * Tight integration with native Rust [functions](https://schungx.github.io/rhai/rust/functions.html) and [types]([#custom-types-and-methods](https://schungx.github.io/rhai/rust/custom.html)), including [getters/setters](https://schungx.github.io/rhai/rust/getters-setters.html), [methods](https://schungx.github.io/rhai/rust/custom.html) and [indexers](https://schungx.github.io/rhai/rust/indexers.html). * Freely pass Rust variables/constants into a script via an external [`Scope`](https://schungx.github.io/rhai/rust/scope.html). * Easily [call a script-defined function](https://schungx.github.io/rhai/engine/call-fn.html) from Rust. @@ -31,10 +31,12 @@ 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 malicious attacks (such as [stack-overflow](https://schungx.github.io/rhai/safety/max-call-stack.html), [over-sized data](https://schungx.github.io/rhai/safety/max-string-size.html), and [runaway scripts](https://schungx.github.io/rhai/safety/max-operations.html) etc.) that may come from untrusted third-party user-land scripts. +* Rugged - protected against malicious attacks (such as [stack-overflow](https://schungx.github.io/rhai/safety/max-call-stack.html), [over-sized data](https://schungx.github.io/rhai/safety/max-string-size.html), and [runaway scripts](https://schungx.github.io/rhai/safety/max-operations.html) etc.) that may come from untrusted third-party user-land scripts. * Track script evaluation [progress](https://schungx.github.io/rhai/safety/progress.html) and manually terminate a script run. * [Function overloading](https://schungx.github.io/rhai/language/overload.html). * [Operator overloading](https://schungx.github.io/rhai/rust/operators.html). +* Dynamic dispatch via [function pointers](https://schungx.github.io/rhai/language/fn-ptr.html). +* Some support for [object-oriented programming (OOP)](https://schungx.github.io/rhai/language/oop.html). * Organize code base with dynamically-loadable [modules](https://schungx.github.io/rhai/language/modules.html). * Scripts are [optimized](https://schungx.github.io/rhai/engine/optimize.html) (useful for template-based machine-generated scripts) for repeated evaluations. * Support for [minimal builds](https://schungx.github.io/rhai/start/builds/minimal.html) by excluding unneeded language [features](https://schungx.github.io/rhai/start/features.html). @@ -42,4 +44,10 @@ Features Documentation ------------- -See [The Rhai Book](https://schungx.github.io/rhai) for details on the Rhai script engine and language. +See [The Rhai Book](https://schungx.github.io/rhai) for details on the Rhai scripting engine and language. + +Playground +---------- + +An [Online Playground](https://alvinhochun.github.io/rhai-demo/) is available with syntax-highlighting editor. +Scripts can be evaluated directly from the editor. diff --git a/RELEASES.md b/RELEASES.md index 33c4418a..6f38bb72 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,18 +1,34 @@ Rhai Release Notes ================== -Version 0.15.2 +Version 0.16.0 ============== +The major new feature in this version is OOP - well, poor man's OOP, that is. + Breaking changes ---------------- * The trait function `ModuleResolver::resolve` no longer takes a `Scope` as argument. +* Functions defined in script now differentiates between using method-call style and normal function-call style. + The method-call style will bind the object to the `this` parameter instead of consuming the first parameter. +* Imported modules are no longer stored in the `Scope`. `Scope::push_module` is removed. + Therefore, cannot rely on module imports to persist across invocations using a `Scope`. +* `AST::retain_functions` is used for another purpose. The old `AST::retain_functions` is renamed to `AST::clear_statements`. + +New features +------------ + +* Support for _function pointers_ via `Fn(name)` and `Fn.call(...)` syntax - a poor man's first-class function. +* Support for calling script-defined functions in method-call style with `this` binding to the object. +* Special support in object maps for OOP. +* Expanded the `AST` API for fine-tuned manipulation of functions. Enhancements ------------ * [The Rhai Book](https://schungx.github.io/rhai) is online. Most content in the original `README` was transferred to the Book. +* New feature `internals` to expose internal data structures (e.g. the AST nodes). Version 0.15.1 diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index da62423b..f9491dd3 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -1,36 +1,37 @@ The Rhai Scripting Language ========================== -1. [What is Rhai](about.md) +1. [What is Rhai](about/index.md) 1. [Features](about/features.md) 2. [Supported Targets and Builds](about/targets.md) 3. [What Rhai Isn't](about/non-design.md) 4. [Related Resources](about/related.md) -2. [Getting Started](start.md) - 1. [Install the Rhai Crate](start/install.md) - 2. [Optional Features](start/features.md) - 3. [Special Builds](start/builds.md) - 1. [Performance Build](start/builds/performance.md) - 2. [Minimal Build](start/builds/minimal.md) - 3. [no-std Build](start/builds/no-std.md) +3. [Getting Started](start/index.md) + 1. [Online Playground](start/playground.md) + 2. [Install the Rhai Crate](start/install.md) + 3. [Optional Features](start/features.md) + 4. [Special Builds](start/builds/index.md) + 1. [Performance](start/builds/performance.md) + 2. [Minimal](start/builds/minimal.md) + 3. [no-std](start/builds/no-std.md) 4. [WebAssembly (WASM)](start/builds/wasm.md) - 4. [Examples](start/examples.md) + 5. [Examples](start/examples/index.md) 1. [Rust](start/examples/rust.md) 2. [Scripts](start/examples/scripts.md) -3. [Using the `Engine`](engine.md) +4. [Using the `Engine`](engine/index.md) 1. [Hello World in Rhai - Evaluate a Script](engine/hello-world.md) 2. [Compile a Script to AST for Repeated Evaluations](engine/compile.md) 3. [Call a Rhai Function from Rust](engine/call-fn.md) 4. [Create a Rust Anonymous Function from a Rhai Function](engine/func.md) 5. [Evaluate Expressions Only](engine/expressions.md) 6. [Raw Engine](engine/raw.md) -4. [Extend Rhai with Rust](rust.md) +5. [Extend Rhai with Rust](rust/index.md) 1. [Traits](rust/traits.md) 2. [Register a Rust Function](rust/functions.md) 1. [String Parameters in Rust Functions](rust/strings.md) 3. [Register a Generic Rust Function](rust/generic.md) 4. [Register a Fallible Rust Function](rust/fallible.md) - 5. [Packages](rust/packages.md) + 5. [Packages](rust/packages/index.md) 1. [Built-in Packages](rust/packages/builtin.md) 2. [Create a Custom Package](rust/packages/create.md) 6. [Override a Built-in Function](rust/override.md) @@ -40,9 +41,9 @@ The Rhai Scripting Language 2. [Indexers](rust/indexers.md) 3. [Disable Custom Types](rust/disable-custom.md) 4. [Printing Custom Types](rust/print-custom.md) - 9. [Scope - Initializing and Maintaining State](rust/scope.md) + 9. [Scope - Initializing and Maintaining State](rust/scope.md) 10. [Engine Configuration Options](rust/options.md) -5. [Rhai Language Reference](language.md) +6. [Rhai Language Reference](language/index.md) 1. [Comments](language/comments.md) 2. [Values and Types](language/values-and-types.md) 1. [Dynamic Values](language/dynamic.md) @@ -56,6 +57,7 @@ The Rhai Scripting Language 5. [Arrays](language/arrays.md) 6. [Object Maps](language/object-maps.md) 1. [Parse from JSON](language/json.md) + 2. [Special Support for OOP](language/object-maps-oop.md) 7. [Time-Stamps](language/timestamps.md) 3. [Keywords](language/keywords.md) 4. [Statements](language/statements.md) @@ -69,17 +71,19 @@ The Rhai Scripting Language 12. [Return Values](language/return.md) 13. [Throw Exception on Error](language/throw.md) 14. [Functions](language/functions.md) - 1. [Function Overloading](language/overload.md) - 2. [Call Method as Function](language/method.md) + 1. [Call Method as Function](language/method.md) + 2. [Overloading](language/overload.md) + 3. [Namespaces](language/fn-namespaces.md) + 4. [Function Pointers](language/fn-ptr.md) 15. [Print and Debug](language/print-debug.md) - 16. [Modules](language/modules.md) + 16. [Modules](language/modules/index.md) 1. [Export Variables, Functions and Sub-Modules](language/modules/export.md) 2. [Import Modules](language/modules/import.md) 3. [Create from Rust](language/modules/rust.md) 4. [Create from AST](language/modules/ast.md) 5. [Module Resolvers](language/modules/resolvers.md) 1. [Implement a Custom Module Resolver](language/modules/imp-resolver.md) -6. [Safety and Protection](safety.md) +7. [Safety and Protection](safety/index.md) 1. [Checked Arithmetic](safety/checked.md) 2. [Sand-Boxing](safety/sandbox.md) 3. [Maximum Length of Strings](safety/max-string-size.md) @@ -90,12 +94,17 @@ The Rhai Scripting Language 7. [Maximum Number of Modules](safety/max-modules.md) 8. [Maximum Call Stack Depth](safety/max-call-stack.md) 9. [Maximum Statement Depth](safety/max-stmt-depth.md) -7. [Advanced Topics](advanced.md) - 1. [Script Optimization](engine/optimize.md) +8. [Advanced Topics](advanced.md) + 1. [Object-Oriented Programming (OOP)](language/oop.md) + 2. [Script Optimization](engine/optimize/index.md) 1. [Optimization Levels](engine/optimize/optimize-levels.md) 2. [Re-Optimize an AST](engine/optimize/reoptimize.md) 3. [Eager Function Evaluation](engine/optimize/eager.md) 4. [Side-Effect Considerations](engine/optimize/side-effects.md) 5. [Volatility Considerations](engine/optimize/volatility.md) 6. [Subtle Semantic Changes](engine/optimize/semantics.md) - 2. [Eval Statement](language/eval.md) + 3. [Eval Statement](language/eval.md) +9. [Appendix](appendix/index.md) + 1. [Keywords](appendix/keywords.md) + 2. [Operators](appendix/operators.md) + 3. [Literals](appendix/literals.md) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 16910c63..c012353a 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -6,9 +6,9 @@ Features Easy ---- -* Easy-to-use language similar to JS+Rust with dynamic typing. +* Easy-to-use language similar to JavaScript+Rust with dynamic typing. -* Tight integration with native Rust [functions]({{rootUrl}}/rust/functions.md) and [types]({{rootUrl}}/rust/custom.md), including [getters/setters]({{rootUrl}}/rust/getters-setters.md), [methods]({{rootUrl}}/rust/custom.md) and [indexers]({{rootUrl}}/rust/indexers.md). +* Tight integration with native Rust [functions] and [types][custom types], including [getters/setters], [methods][custom type] and [indexers]. * Freely pass Rust variables/constants into a script via an external [`Scope`]. @@ -24,7 +24,7 @@ Fast * Fairly efficient evaluation (1 million iterations in 0.25 sec on a single core, 2.3 GHz Linux VM). -* Scripts are [optimized]({{rootUrl}}/engine/optimize.md) (useful for template-based machine-generated scripts) for repeated evaluations. +* Scripts are [optimized][script optimization] (useful for template-based machine-generated scripts) for repeated evaluations. Dynamic ------- @@ -35,6 +35,10 @@ Dynamic * Organize code base with dynamically-loadable [modules]. +* Dynamic dispatch via [function pointers]. + +* Some support for [object-oriented programming (OOP)][OOP]. + Safe ---- @@ -46,7 +50,7 @@ Rugged * Sand-boxed - the scripting [`Engine`], if declared immutable, cannot mutate the containing environment unless explicitly permitted (e.g. via a `RefCell`). -* Protected against malicious attacks (such as [stack-overflow]({{rootUrl}}/safety/max-call-stack.md), [over-sized data]({{rootUrl}}/safety/max-string-size.md), and [runaway scripts]({{rootUrl}}/safety/max-operations.md) etc.) that may come from untrusted third-party user-land scripts. +* Protected 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] and manually terminate a script run. diff --git a/doc/src/about.md b/doc/src/about/index.md similarity index 86% rename from doc/src/about.md rename to doc/src/about/index.md index 8a15d31f..85c55824 100644 --- a/doc/src/about.md +++ b/doc/src/about/index.md @@ -1,7 +1,7 @@ What is Rhai ============ -{{#include links.md}} +{{#include ../links.md}} Rhai is an embedded scripting language and evaluation engine for Rust that gives a safe and easy way to add scripting to any application. diff --git a/doc/src/about/non-design.md b/doc/src/about/non-design.md index 2b07946e..da3747df 100644 --- a/doc/src/about/non-design.md +++ b/doc/src/about/non-design.md @@ -11,10 +11,15 @@ It doesn't attempt to be a new language. For example: * No traits... so it is also not Rust. Do your Rusty stuff in Rust. * No structures/records - define your types in Rust instead; Rhai can seamlessly work with _any Rust type_. + There is, however, a built-in [object map] type which is adequate for most uses. + It is possible to simulate [object-oriented programming (OOP)][OOP] by storing [function pointers] + in [object map] properties, turning them into _methods_. * No first-class functions - Code your functions in Rust instead, and register them with Rhai. + There is, however, support for simple [function pointers] allowing runtime dispatch by function name. + * No garbage collection - this should be expected, so... * No closures - do your closure magic in Rust instead; [turn a Rhai scripted function into a Rust closure]({{rootUrl}}/engine/call-fn.md). @@ -26,7 +31,7 @@ Due to this intended usage, Rhai deliberately keeps the language simple and smal such as classes, inheritance, first-class functions, closures, concurrency, byte-codes, JIT etc. Avoid the temptation to write full-fledge program logic entirely in Rhai - that use case is best fulfilled by -more complete languages such as JS or Lua. +more complete languages such as JavaScript or Lua. Therefore, in actual practice, it is usually best to expose a Rust API into Rhai for scripts to call. All your core functionalities should be in Rust. diff --git a/doc/src/about/related.md b/doc/src/about/related.md index b6d8d8df..defc29e4 100644 --- a/doc/src/about/related.md +++ b/doc/src/about/related.md @@ -5,8 +5,13 @@ Related Resources Other online documentation resources for Rhai: -* [`DOCS.RS`](https://docs.rs/rhai) +* [`crates.io`](https://crates.io/crates/rhai/) - Rhai crate +* [`DOCS.RS`](https://docs.rs/rhai) - Rhai API documentation + +* [`LIB.RS`](https://lib.rs/crates/rhai) - Rhai library info + +* [Online Playground][playground] - Run scripts directly from editor Other cool projects to check out: diff --git a/doc/src/advanced.md b/doc/src/advanced.md index d1982c2c..3cb28cb5 100644 --- a/doc/src/advanced.md +++ b/doc/src/advanced.md @@ -7,4 +7,4 @@ This section covers advanced features such as: * [Script optimization] -* The dreaded (or beloved depending on your taste) [`eval`] statement +* The dreaded (or beloved for those with twisted tastes) [`eval`] statement diff --git a/doc/src/appendix/index.md b/doc/src/appendix/index.md new file mode 100644 index 00000000..bdb6cae9 --- /dev/null +++ b/doc/src/appendix/index.md @@ -0,0 +1,6 @@ +Appendix +======== + +{{#include ../links.md}} + +This section contains miscellaneous reference materials. diff --git a/doc/src/appendix/keywords.md b/doc/src/appendix/keywords.md new file mode 100644 index 00000000..660a605c --- /dev/null +++ b/doc/src/appendix/keywords.md @@ -0,0 +1,33 @@ +Keywords List +============= + +{{#include ../links.md}} + +| Keyword | Description | Not available under | +| :-------------------: | ---------------------------------------- | :-----------------: | +| `true` | Boolean true literal | | +| `false` | Boolean false literal | | +| `let` | Variable declaration | | +| `const` | Constant declaration | | +| `if` | If statement | | +| `else` | else block of if statement | | +| `while` | While loop | | +| `loop` | Infinite loop | | +| `for` | For loop | | +| `in` | Containment test, part of for loop | | +| `continue` | Continue a loop at the next iteration | | +| `break` | Loop breaking | | +| `return` | Return value | | +| `throw` | Throw exception | | +| `import` | Import module | [`no_module`] | +| `export` | Export variable | [`no_module`] | +| `as` | Alias for variable export | [`no_module`] | +| `private` | Mark function private | [`no_function`] | +| `fn` (lower-case `f`) | Function definition | [`no_function`] | +| `Fn` (capital `F`) | Function to create a [function pointer] | | +| `call` | Call a [function pointer] | | +| `this` | Reference to base object for method call | [`no_function`] | +| `type_of` | Get type name of value | | +| `print` | Print value | | +| `debug` | Print value in debug format | | +| `eval` | Evaluate script | | diff --git a/doc/src/appendix/literals.md b/doc/src/appendix/literals.md new file mode 100644 index 00000000..a308e951 --- /dev/null +++ b/doc/src/appendix/literals.md @@ -0,0 +1,16 @@ +Literals Syntax +=============== + +{{#include ../links.md}} + +| Type | Literal syntax | +| :--------------------------------: | :------------------------------------------------------------------------------: | +| `INT` | `42`, `-123`, `0`,
`0x????..` (hex), `0b????..` (binary), `0o????..` (octal) | +| `FLOAT` | `42.0`, `-123.456`, `0.0` | +| [String] | `"... \x?? \u???? \U???????? ..."` | +| Character | `"... \x?? \u???? \U???????? ..."` | +| [`Array`] | `[ ???, ???, ??? ]` | +| [Object map] | `#{ a: ???, b: ???, c: ???, "def": ??? }` | +| Boolean true | `true` | +| Boolean false | `false` | +| `Nothing`/`null`/`nil`/`void`/Unit | `()` | diff --git a/doc/src/appendix/operators.md b/doc/src/appendix/operators.md new file mode 100644 index 00000000..56782b95 --- /dev/null +++ b/doc/src/appendix/operators.md @@ -0,0 +1,30 @@ +Operators +========= + +{{#include ../links.md}} + +| Operator | Description | Binary? | +| :---------------: | ------------------------------ | :-----: | +| `+` | Add | Yes | +| `-` | Subtract, Minus | Yes/No | +| `*` | Multiply | Yes | +| `/` | Divide | Yes | +| `%` | Modulo | Yes | +| `~` | Power | Yes | +| `>>` | Right bit-shift | Yes | +| `<<` | Left bit-shift | Yes | +| `&` | Bit-wise _And_, Boolean _And_ | Yes | +| \| | Bit-wise _Or_, Boolean _Or_ | Yes | +| `^` | Bit-wise _Xor_ | Yes | +| `==` | Equals to | Yes | +| `~=` | Not equals to | Yes | +| `>` | Greater than | Yes | +| `>=` | Greater than or equals to | Yes | +| `<` | Less than | Yes | +| `<=` | Less than or equals to | Yes | +| `>=` | Greater than or equals to | Yes | +| `&&` | Boolean _And_ (short-circuits) | Yes | +| \|\| | Boolean _Or_ (short-circuits) | Yes | +| `!` | Boolean _Not_ | No | +| `[` .. `]` | Indexing | Yes | +| `.` | Property access, Method call | Yes | diff --git a/doc/src/context.json b/doc/src/context.json index f898a0c5..6da8d6bb 100644 --- a/doc/src/context.json +++ b/doc/src/context.json @@ -1,4 +1,5 @@ { + "version": "0.16.0", "rootUrl": "", "rootUrlX": "/rhai" } \ No newline at end of file diff --git a/doc/src/engine/compile.md b/doc/src/engine/compile.md index 32e16bce..f4ee0e65 100644 --- a/doc/src/engine/compile.md +++ b/doc/src/engine/compile.md @@ -3,7 +3,7 @@ Compile a Script (to AST) {{#include ../links.md}} -To repeatedly evaluate a script, _compile_ it first into an AST (abstract syntax tree) form: +To repeatedly evaluate a script, _compile_ it first into an `AST` (abstract syntax tree) form: ```rust // Compile to an AST and store it for later evaluations diff --git a/doc/src/engine.md b/doc/src/engine/index.md similarity index 88% rename from doc/src/engine.md rename to doc/src/engine/index.md index 5ed3389a..8bf58504 100644 --- a/doc/src/engine.md +++ b/doc/src/engine/index.md @@ -1,7 +1,7 @@ Using the Engine ================ -{{#include links.md}} +{{#include ../links.md}} Rhai's interpreter resides in the [`Engine`] type under the master `rhai` namespace. diff --git a/doc/src/engine/optimize.md b/doc/src/engine/optimize/index.md similarity index 99% rename from doc/src/engine/optimize.md rename to doc/src/engine/optimize/index.md index 10ee4f21..e28aa748 100644 --- a/doc/src/engine/optimize.md +++ b/doc/src/engine/optimize/index.md @@ -1,7 +1,7 @@ Script Optimization =================== -{{#include ../links.md}} +{{#include ../../links.md}} Rhai includes an _optimizer_ that tries to optimize a script after parsing. This can reduce resource utilization and increase execution speed. diff --git a/doc/src/engine/optimize/optimize-levels.md b/doc/src/engine/optimize/optimize-levels.md index 11511099..97dc27ea 100644 --- a/doc/src/engine/optimize/optimize-levels.md +++ b/doc/src/engine/optimize/optimize-levels.md @@ -3,10 +3,7 @@ Optimization Levels {{#include ../../links.md}} -Set Optimization Level ---------------------- - -There are actually three levels of optimizations: `None`, `Simple` and `Full`. +There are three levels of optimization: `None`, `Simple` and `Full`. * `None` is obvious - no optimization on the AST is performed. @@ -16,6 +13,10 @@ There are actually three levels of optimizations: `None`, `Simple` and `Full`. * `Full` is _much_ more aggressive, _including_ running functions on constant arguments to determine their result. One benefit to this is that many more optimization opportunities arise, especially with regards to comparison operators. + +Set Optimization Level +--------------------- + An [`Engine`]'s optimization level is set via a call to `Engine::set_optimization_level`: ```rust diff --git a/doc/src/engine/optimize/reoptimize.md b/doc/src/engine/optimize/reoptimize.md index d3ac0f26..e8292e4c 100644 --- a/doc/src/engine/optimize/reoptimize.md +++ b/doc/src/engine/optimize/reoptimize.md @@ -3,15 +3,38 @@ Re-Optimize an AST {{#include ../../links.md}} -If it is ever needed to _re_-optimize an `AST`, use the `optimize_ast` method: +Sometimes it is more efficient to store one single, large script with delimited code blocks guarded by +constant variables. This script is compiled once to an [`AST`]. + +Then, depending on the execution environment, constants are passed into the [`Engine`] and the [`AST`] +is _re_-optimized based on those constants via the `Engine::optimize_ast` method, +effectively pruning out unused code sections. + +The final, optimized [`AST`] is then used for evaluations. ```rust -// Compile script to AST -let ast = engine.compile("40 + 2")?; +// Compile master script to AST +let master_ast = engine.compile( +r" + if SCENARIO_1 { + do_work(); + } else if SCENARIO_2 { + do_something(); + } else if SCENARIO_3 { + do_something_else(); + } else { + do_nothing(); + } +")?; -// Create a new 'Scope' - put constants in it to aid optimization if using 'OptimizationLevel::Full' -let scope = Scope::new(); +// Create a new 'Scope' - put constants in it to aid optimization +let mut scope = Scope::new(); +scope.push_constant("SCENARIO_1", true); +scope.push_constant("SCENARIO_2", false); +scope.push_constant("SCENARIO_3", false); // Re-optimize the AST -let ast = engine.optimize_ast(&scope, &ast, OptimizationLevel::Full); +let new_ast = engine.optimize_ast(&scope, master_ast.clone(), OptimizationLevel::Simple); + +// 'new_ast' is essentially: 'do_work()' ``` diff --git a/doc/src/language/arrays.md b/doc/src/language/arrays.md index 5730d862..3afc09b6 100644 --- a/doc/src/language/arrays.md +++ b/doc/src/language/arrays.md @@ -22,7 +22,7 @@ The maximum allowed size of an array can be controlled via `Engine::set_max_arra Built-in Functions ----------------- -The following methods (mostly defined in the [`BasicArrayPackage`]({{rootUrl}}/rust/packages.md) but excluded if using a [raw `Engine`]) operate on arrays: +The following methods (mostly defined in the [`BasicArrayPackage`][packages] but excluded if using a [raw `Engine`]) operate on arrays: | Function | Parameter(s) | Description | | ------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | @@ -38,6 +38,7 @@ The following methods (mostly defined in the [`BasicArrayPackage`]({{rootUrl}}/r | `clear` | _none_ | empties the array | | `truncate` | target length | cuts off the array at exactly a specified length (discarding all subsequent elements) | + Examples -------- diff --git a/doc/src/language/constants.md b/doc/src/language/constants.md index 82811166..d88ecd4f 100644 --- a/doc/src/language/constants.md +++ b/doc/src/language/constants.md @@ -9,11 +9,14 @@ Constants follow the same naming rules as [variables]. ```rust const x = 42; + print(x * 2); // prints 84 + x = 123; // <- syntax error: cannot assign to constant ``` -Constants must be assigned a _value_, not an expression. +Unlike variables which need not have initial values (default to [`()`]), +constants must be assigned one, and it must be a constant _value_, not an expression. ```rust const x = 40 + 2; // <- syntax error: cannot assign expression to constant diff --git a/doc/src/language/convert.md b/doc/src/language/convert.md index a0cd7a83..0c2226eb 100644 --- a/doc/src/language/convert.md +++ b/doc/src/language/convert.md @@ -11,10 +11,14 @@ That's it; for other conversions, register custom conversion functions. ```rust let x = 42; + let y = x * 100.0; // <- error: cannot multiply i64 with f64 + let y = x.to_float() * 100.0; // works + let z = y.to_int() + x; // works let c = 'X'; // character + print("c is '" + c + "' and its code is " + c.to_int()); // prints "c is 'X' and its code is 88" ``` diff --git a/doc/src/language/dynamic.md b/doc/src/language/dynamic.md index e8c8a21b..9ebe7d6f 100644 --- a/doc/src/language/dynamic.md +++ b/doc/src/language/dynamic.md @@ -27,6 +27,8 @@ if type_of(mystery) == "i64" { print("Hey, I got an array here!"); } else if type_of(mystery) == "map" { print("Hey, I got an object map here!"); +} else if type_of(mystery) == "Fn" { + print("Hey, I got a function pointer here!"); } else if type_of(mystery) == "TestStruct" { print("Hey, I got the TestStruct custom type here!"); } else { diff --git a/doc/src/language/eval.md b/doc/src/language/eval.md index f79810e5..a7e5b979 100644 --- a/doc/src/language/eval.md +++ b/doc/src/language/eval.md @@ -6,29 +6,29 @@ Or "How to Shoot Yourself in the Foot even Easier" ------------------------------------------------ -Saving the best for last: in addition to script optimizations, there is the ever-dreaded... `eval` function! +Saving the best for last, there is the ever-dreaded... `eval` function! ```rust let x = 10; fn foo(x) { x += 12; x } -let script = "let y = x;"; // build a script +let script = "let y = x;"; // build a script script += "y += foo(y);"; script += "x + y"; -let result = eval(script); // <- look, JS, we can also do this! +let result = eval(script); // <- look, JavaScript, we can also do this! -print("Answer: " + result); // prints 42 +print("Answer: " + result); // prints 42 -print("x = " + x); // prints 10: functions call arguments are passed by value -print("y = " + y); // prints 32: variables defined in 'eval' persist! +print("x = " + x); // prints 10: functions call arguments are passed by value +print("y = " + y); // prints 32: variables defined in 'eval' persist! -eval("{ let z = y }"); // to keep a variable local, use a statement block +eval("{ let z = y }"); // to keep a variable local, use a statement block -print("z = " + z); // <- error: variable 'z' not found +print("z = " + z); // <- error: variable 'z' not found -"print(42)".eval(); // <- nope... method-call style doesn't work +"print(42)".eval(); // <- nope... method-call style doesn't work with 'eval' ``` Script segments passed to `eval` execute inside the current [`Scope`], so they can access and modify _everything_, @@ -45,8 +45,8 @@ not inside another function call! ```rust let script = "x += 32"; let x = 10; -eval(script); // variable 'x' in the current scope is visible! -print(x); // prints 42 +eval(script); // variable 'x' in the current scope is visible! +print(x); // prints 42 // The above is equivalent to: let script = "x += 32"; @@ -65,7 +65,7 @@ disable `eval` by overloading it, probably with something that throws. ```rust fn eval(script) { throw "eval is evil! I refuse to run " + script } -let x = eval("40 + 2"); // 'eval' here throws "eval is evil! I refuse to run 40 + 2" +let x = eval("40 + 2"); // 'eval' here throws "eval is evil! I refuse to run 40 + 2" ``` Or overload it from Rust: @@ -82,15 +82,15 @@ engine.register_result_fn("eval", alt_eval); `EvalPackage` ------------- -There is even a package named [`EvalPackage`]({{rootUrl}}/rust/packages.md) which implements the disabling override: +There is even a package named [`EvalPackage`][packages] which implements the disabling override: ```rust use rhai::Engine; -use rhai::packages::Package // load the 'Package' trait to use packages -use rhai::packages::EvalPackage; // the 'eval' package disables 'eval' +use rhai::packages::Package // load the 'Package' trait to use packages +use rhai::packages::EvalPackage; // the 'eval' package disables 'eval' let mut engine = Engine::new(); -let package = EvalPackage::new(); // create the package +let package = EvalPackage::new(); // create the package -engine.load_package(package.get()); // load the package +engine.load_package(package.get()); // load the package ``` diff --git a/doc/src/language/fn-namespaces.md b/doc/src/language/fn-namespaces.md new file mode 100644 index 00000000..5bd8e16f --- /dev/null +++ b/doc/src/language/fn-namespaces.md @@ -0,0 +1,141 @@ +Function Namespaces +================== + +{{#include ../links.md}} + +Each Function is a Separate Compilation Unit +------------------------------------------- + +[Functions] in Rhai are _pure_ and they form individual _compilation units_. +This means that individual functions can be separated, exported, re-grouped, imported, +and generally mix-'n-match-ed with other completely unrelated scripts. + +For example, the `AST::merge` method allows merging all functions in one [`AST`] into another, +forming a new, combined, group of functions. + +In general, there are two types of _namespaces_ where functions are looked up: + +| Namespace | Source | Lookup method | How Many | +| --------- | ---------------------------------------------------------------------- | --------------------------------- | :----------------------: | +| Global | `Engine::register_XXX` API, [`AST`] being evaluated, [packages] loaded | Simple function name | One | +| Module | [`Module`] | Namespace-qualified function name | As many as [`import`]-ed | + + +Global Namespace +---------------- + +There is one _global_ namespace for every [`Engine`], which includes: + +* All the native Rust functions registered via the `Engine::register_XXX` API. + +* All the Rust functions defined in [packages] that are loaded into the [`Engine`]. + +In addition, during evaluation of an [`AST`], all script-defined functions bundled together within +the [`AST`] are added to the global namespace and override any existing registered functions of +the same names and number of parameters. + +Anywhere in a Rhai script, when a function call is made, it is searched within the global namespace. +Therefore, function calls in Rhai are _late_ bound - meaning that the function called cannot be +determined or guaranteed and there is no way to _lock down_ the function being called. +This aspect is very similar to JavaScript before ES6 modules. + +```rust +// Compile a script into AST +let ast1 = engine.compile( + r#" + fn message() { "Hello!" } // greeting message + + fn say_hello() { + print(message()); // prints message + } + + say_hello(); + "# +)?; + +// Compile another script with an overriding function +let ast2 = engine.compile(r#"fn message() { "Boo!" }"#)?; + +// Merge the two AST's +let ast = ast1.merge(ast2); // 'message' will be overwritten + +engine.consume_ast(&ast)?; // prints 'Boo!' +``` + +Therefore, care must be taken when _cross-calling_ functions to make sure that the correct +functions are called. + +The only practical way to ensure that a function is a correct one is to use [modules] - +i.e. define the function in a separate module and then [`import`] it: + +```rust +message.rhai: + + fn message() { "Hello!" } + +script.rhai: + + fn say_hello() { + import "message" as msg; + print(msg::message()); + } + say_hello(); +``` + + +Module Namespaces +----------------- + +[Modules] can be dynamically loaded into a Rhai script using the [`import`] keyword. +When that happens, functions defined within the [module] can be called with a _qualified_ name. + +There is a catch, though, if functions in a module script refer to global functions +defined _within the script_. When called later, those functions will be searched in the +current global namespace and may not be found. + +```rust +greeting.rhai: + + fn message() { "Hello!" }; + + fn say_hello() { print(message()); } + + say_hello(); // 'message' is looked up in the global namespace + +script.rhai: + + import "greeting" as g; + g::say_hello(); // <- error: function not found - 'message' +``` + +In the example above, although the module `greeting.rhai` loads fine (`"Hello!"` is printed), +the subsequent call using the _namespace-qualified_ function name fails to find the same function +'`message`' which now essentially becomes `g::message`. The call fails as there is no more +function named '`message`' in the global namespace. + +Therefore, when writing functions for a [module], make sure that those functions are as _pure_ +as possible and avoid cross-calling them from each other. A [function pointer] is a valid technique +to call another function within a module-defined function: + +```rust +greeting.rhai: + + fn message() { "Hello!" }; + + fn say_hello(msg_func) { // 'msg_func' is a function pointer + print(msg_func.call()); // call via the function pointer + } + + say_hello(); // 'message' is looked up in the global namespace + +script.rhai: + + import "greeting" as g; + + fn my_msg() { + import "greeting" as g; // <- must import again here... + g::message() // <- ... otherwise will not find module 'g' + } + + g::say_hello(Fn("my_msg")); // prints 'Hello!' +``` diff --git a/doc/src/language/fn-ptr.md b/doc/src/language/fn-ptr.md new file mode 100644 index 00000000..e4d9bc5d --- /dev/null +++ b/doc/src/language/fn-ptr.md @@ -0,0 +1,136 @@ +Function Pointers +================= + +{{#include ../links.md}} + +It is possible to store a _function pointer_ in a variable just like a normal value. +In fact, internally a function pointer simply stores the _name_ of the function as a string. + +Call a function pointer using the `call` method, which needs to be called in method-call style. + + +Built-in methods +---------------- + +The following standard methods (mostly defined in the [`BasicFnPackage`][packages] but excluded if +using a [raw `Engine`]) operate on [strings]: + +| Function | Parameter(s) | Description | +| -------------------------- | ------------ | --------------------------------------------------------------------- | +| `name` method and property | _none_ | returns the name of the function encapsulated by the function pointer | + + +Examples +-------- + +```rust +fn foo(x) { 41 + x } + +let func = Fn("foo"); // use the 'Fn' function to create a function pointer + +print(func); // prints 'Fn(foo)' + +let func = fn_name.Fn(); // <- error: 'Fn' cannot be called in method-call style + +func.type_of() == "Fn"; // type_of() as function pointer is 'Fn' + +func.name == "foo"; + +func.call(1) == 42; // call a function pointer with the 'call' method + +foo(1) == 42; // <- the above de-sugars to this + +call(func, 1); //<- error: 'call (Fn, i64)' is not a registered function + +let len = Fn("len"); // 'Fn' also works with registered native Rust functions + +len.call("hello") == 5; + +let add = Fn("+"); // 'Fn' works with built-in operators also + +add.call(40, 2) == 42; + +let fn_name = "hello"; // the function name does not have to exist yet + +let hello = Fn(fn_name + "_world"); + +hello.call(0); // error: function not found - 'hello_world (i64)' +``` + + +Global Namespace Only +-------------------- + +Because of their dynamic nature, function pointers cannot refer to functions in a _module_ [namespace][function namespace] +(i.e. functions in [`import`]-ed modules). They can only refer to functions within the global [namespace][function namespace]. +See [function namespaces] for more details. + +```rust +import "foo" as f; // assume there is 'f::do_something()' + +f::do_something(); // works! + +let p = Fn("f::do_something"); + +p.call(); // error: function not found - 'f::do_something' + +fn do_something_now() { // call it from a local function + import "foo" as f; + f::do_something(); +} + +let p = Fn("do_something_now"); + +p.call(); // works! +``` + + +Dynamic Dispatch +---------------- + +The purpose of function pointers is to enable rudimentary _dynamic dispatch_, meaning to determine, +at runtime, which function to call among a group. + +Although it is possible to simulate dynamic dispatch via a number and a large `if-then-else-if` statement, +using function pointers significantly simplifies the code. + +```rust +let x = some_calculation(); + +// These are the functions to call depending on the value of 'x' +fn method1(x) { ... } +fn method2(x) { ... } +fn method3(x) { ... } + +// Traditional - using decision variable +let func = sign(x); + +// Dispatch with if-statement +if func == -1 { + method1(42); +} else if func == 0 { + method2(42); +} else if func == 1 { + method3(42); +} + +// Using pure function pointer +let func = if x < 0 { + Fn("method1") +} else if x == 0 { + Fn("method2") +} else if x > 0 { + Fn("method3") +} + +// Dynamic dispatch +func.call(42); + +// Using functions map +let map = [ Fn("method1"), Fn("method2"), Fn("method3") ]; + +let func = sign(x) + 1; + +// Dynamic dispatch +map[func].call(42); +``` diff --git a/doc/src/language/for.md b/doc/src/language/for.md index eb2406fc..6780a1cd 100644 --- a/doc/src/language/for.md +++ b/doc/src/language/for.md @@ -5,50 +5,63 @@ Iterating through a range or an [array] is provided by the `for` ... `in` loop. +Like C, `continue` can be used to skip to the next iteration, by-passing all following statements; +`break` can be used to break out of the loop unconditionally. + ```rust // Iterate through string, yielding characters let s = "hello, world!"; for ch in s { - if ch > 'z' { continue; } // skip to the next iteration + if ch > 'z' { continue; } // skip to the next iteration + print(ch); - if x == '@' { break; } // break out of for loop + + 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 + if x > 10 { continue; } // skip to the next iteration + print(x); - if x == 42 { break; } // break out of for loop + + if x == 42 { break; } // break out of for loop } // The 'range' function allows iterating from first to last-1 for x in range(0, 50) { - if x > 10 { continue; } // skip to the next iteration + if x > 10 { continue; } // skip to the next iteration + print(x); - if x == 42 { break; } // break out of for loop + + if x == 42 { break; } // break out of for loop } // The 'range' function also takes a step -for x in range(0, 50, 3) { // step by 3 - if x > 10 { continue; } // skip to the next iteration +for x in range(0, 50, 3) { // step by 3 + if x > 10 { continue; } // skip to the next iteration + print(x); - if x == 42 { break; } // break out of for loop + + if x == 42 { break; } // break out of for loop } // Iterate through object map let map = #{a:1, b:3, c:5, d:7, e:9}; -// Property names are returned in random order +// Property names are returned in unsorted, random order for x in keys(map) { - if x > 10 { continue; } // skip to the next iteration + if x > 10 { continue; } // skip to the next iteration + print(x); - if x == 42 { break; } // break out of for loop + + if x == 42 { break; } // break out of for loop } -// Property values are returned in random order +// Property values are returned in unsorted, random order for val in values(map) { print(val); } diff --git a/doc/src/language/functions.md b/doc/src/language/functions.md index d2cb48d5..72dac0b4 100644 --- a/doc/src/language/functions.md +++ b/doc/src/language/functions.md @@ -18,6 +18,7 @@ print(add(2, 3)); // prints 5 print(sub(2, 3,)); // prints -1 - trailing comma in arguments list is OK ``` + Implicit Return --------------- @@ -38,6 +39,7 @@ print(add(2, 3)); // prints 5 print(add2(42)); // prints 44 ``` + No Access to External Scope -------------------------- @@ -50,26 +52,6 @@ let x = 42; fn foo() { x } // <- syntax error: variable 'x' doesn't exist ``` -Passing Arguments by Value -------------------------- - -Functions defined in script always take [`Dynamic`] parameters (i.e. the parameter can be of any type). -It is important to remember that all arguments are passed by _value_, so all functions are _pure_ -(i.e. they never modify their arguments). - -Any update to an argument will **not** be reflected back to the caller. - -This can introduce subtle bugs, if not careful, especially when using the _method-call_ style. - -```rust -fn change(s) { // 's' is passed by value - s = 42; // only a COPY of 's' is changed -} - -let x = 500; -x.change(); // de-sugars to 'change(x)' -x == 500; // 'x' is NOT changed! -``` Global Definitions Only ---------------------- @@ -92,6 +74,56 @@ fn do_addition(x) { } ``` -Unlike C/C++, functions can be defined _anywhere_ within the global level. A function does not need to be defined -prior to being used in a script; a statement in the script can freely call a function defined afterwards. -This is similar to Rust and many other modern languages. + +Use Before Definition +-------------------- + +Unlike C/C++, functions in Rhai can be defined _anywhere_ at global level. +A function does not need to be defined prior to being used in a script; +a statement in the script can freely call a function defined afterwards. + +This is similar to Rust and many other modern languages, such as JavaScript's `function` keyword. + + +Arguments Passed by Value +------------------------ + +Functions defined in script always take [`Dynamic`] parameters (i.e. the parameter can be of any type). +Therefore, functions with the same name and same _number_ of parameters are equivalent. + +It is important to remember that all arguments are passed by _value_, so all Rhai script-defined functions +are _pure_ (i.e. they never modify their arguments). +Any update to an argument will **not** be reflected back to the caller. + +```rust +fn change(s) { // 's' is passed by value + s = 42; // only a COPY of 's' is changed +} + +let x = 500; + +change(x); + +x == 500; // 'x' is NOT changed! +``` + + +`this` - Simulating an Object Method +----------------------------------- + +Functions can also be called in method-call style. When this is the case, the keyword '`this`' +binds to the object in the method call and can be changed. + +```rust +fn change() { // not that the object does not need a parameter + this = 42; // 'this' binds to the object in method-call +} + +let x = 500; + +x.change(); // call 'change' in method-call style, 'this' binds to 'x' + +x == 42; // 'x' is changed! + +change(); // <- error: `this` is unbounded +``` diff --git a/doc/src/language/if.md b/doc/src/language/if.md index a71cbe9a..3958a2e3 100644 --- a/doc/src/language/if.md +++ b/doc/src/language/if.md @@ -3,24 +3,30 @@ {{#include ../links.md}} +`if` statements follow C syntax: + ```rust if foo(x) { print("It's true!"); } else if bar == baz { print("It's true again!"); -} else if ... { - : -} else if ... { - : +} else if baz.is_foo() { + print("Yet again true."); +} else if foo(bar - baz) { + print("True again... this is getting boring."); } else { print("It's finally false!"); } ``` -All branches of an `if` statement must be enclosed within braces '`{`' .. '`}`', even when there is only one statement. -Like Rust, there is no ambiguity regarding which `if` clause a statement belongs to. +Unlike C, the condition expression does _not_ need to be enclosed in parentheses '`(`' .. '`)`', but +all branches of the `if` statement must be enclosed within braces '`{`' .. '`}`', +even when there is only one statement inside the branch. + +Like Rust, there is no ambiguity regarding which `if` clause a branch belongs to. ```rust +// Rhai is not C! if (decision) print("I've decided!"); // ^ syntax error, expecting '{' in statement block ``` diff --git a/doc/src/language.md b/doc/src/language/index.md similarity index 78% rename from doc/src/language.md rename to doc/src/language/index.md index b3526ebd..ac4e4fb6 100644 --- a/doc/src/language.md +++ b/doc/src/language/index.md @@ -1,7 +1,7 @@ Rhai Language Reference ====================== -{{#include links.md}} +{{#include ../links.md}} This section outlines the Rhai language. diff --git a/doc/src/language/keywords.md b/doc/src/language/keywords.md index 48c32015..836e9b0c 100644 --- a/doc/src/language/keywords.md +++ b/doc/src/language/keywords.md @@ -13,8 +13,10 @@ The following are reserved keywords in Rhai: | `while`, `loop`, `for`, `in`, `continue`, `break` | Looping | | | `fn`, `private` | Functions | [`no_function`] | | `return` | Return values | | -| `throw` | Return errors | | +| `throw` | throw exceptions | | | `import`, `export`, `as` | Modules | [`no_module`] | +| `Fn`, `call` | Function pointers | | +| `type_of`, `print`, `debug`, `eval` | Special functions | | -Keywords cannot be the name of a [function] or [variable], unless the relevant exclusive feature is enabled. +Keywords cannot be the name of a [function] or [variable], unless the relevant feature is enabled. For example, `fn` is a valid variable name under [`no_function`]. diff --git a/doc/src/language/logic.md b/doc/src/language/logic.md index 7867a3d8..113641f6 100644 --- a/doc/src/language/logic.md +++ b/doc/src/language/logic.md @@ -13,8 +13,11 @@ set of types (see [built-in operators]). ```rust 42 == 42; // true + 42 > 42; // false + "hello" > "foo"; // true + "42" == 42; // false ``` @@ -23,13 +26,17 @@ except for '`!=`' (not equals) which results in `true`. This is in line with int ```rust 42 == 42.0; // false - i64 cannot be compared with f64 + 42 != 42.0; // true - i64 cannot be compared with f64 42 > "42"; // false - i64 cannot be compared with string + 42 <= "42"; // false - i64 cannot be compared with string let ts = new_ts(); // custom type + ts == 42; // false - types cannot be compared + ts != 42; // true - types cannot be compared ``` @@ -50,25 +57,42 @@ if the first one already proves the condition wrong. Single boolean operators `&` and `|` always evaluate both operands. ```rust -this() || that(); // that() is not evaluated if this() is true -this() && that(); // that() is not evaluated if this() is false +a() || b(); // b() is not evaluated if a() is true -this() | that(); // both this() and that() are evaluated -this() & that(); // both this() and that() are evaluated +a() && b(); // b() is not evaluated if a() is false + +a() | b(); // both a() and b() are evaluated + +a() & b(); // both a() and b() are evaluated ``` Compound Assignment Operators ---------------------------- ```rust -let number = 5; -number += 4; // number = number + 4 -number -= 3; // number = number - 3 -number *= 2; // number = number * 2 -number /= 1; // number = number / 1 -number %= 3; // number = number % 3 +let number = 9; + +number += 8; // number = number + 8 + +number -= 7; // number = number - 7 + +number *= 6; // number = number * 6 + +number /= 5; // number = number / 5 + +number %= 4; // number = number % 4 + +number ~= 3; // number = number ~ 3 + number <<= 2; // number = number << 2 + number >>= 1; // number = number >> 1 + +number &= 0x00ff; // number = number & 0x00ff; + +number |= 0x00ff; // number = number | 0x00ff; + +number ^= 0x00ff; // number = number ^ 0x00ff; ``` The `+=` operator can also be used to build [strings]: diff --git a/doc/src/language/loop.md b/doc/src/language/loop.md index 8b1cba8e..b92c3f7e 100644 --- a/doc/src/language/loop.md +++ b/doc/src/language/loop.md @@ -3,13 +3,24 @@ Infinite `loop` {{#include ../links.md}} +Infinite loops follow C syntax. + +Like C, `continue` can be used to skip to the next iteration, by-passing all following statements; +`break` can be used to break out of the loop unconditionally. + ```rust let x = 10; loop { x = x - 1; + if x > 5 { continue; } // skip to the next iteration + print(x); + if x == 0 { break; } // break out of loop } ``` + +Beware: a `loop` statement without a `break` statement inside its loop block is infinite - +there is no way for the loop to stop iterating. diff --git a/doc/src/language/method.md b/doc/src/language/method.md index 23822e05..bdce0ed6 100644 --- a/doc/src/language/method.md +++ b/doc/src/language/method.md @@ -3,11 +3,16 @@ Call Method as Function {{#include ../links.md}} -Properties and methods in a Rust custom type registered with the [`Engine`] can be called just like a regular function in Rust. +Property [getters/setters] and [methods][custom types] in a Rust custom type registered with the [`Engine`] can be called +just like a regular function. In fact, like Rust, property getters/setters and object methods +are registered as regular [functions] in Rhai that take a first `&mut` parameter. Unlike functions defined in script (for which all arguments are passed by _value_), native Rust functions may mutate the object (or the first argument if called in normal function call style). +However, sometimes it is not as straight-forward, and methods called in function-call style may end up +not muting the object - see the example below. Therefore, it is best to always use method-call style. + Custom types, properties and methods can be disabled via the [`no_object`] feature. ```rust @@ -21,8 +26,8 @@ update(a); // <- this de-sugars to 'a.update()' thus if 'a' is a simple let array = [ a ]; update(array[0]); // <- 'array[0]' is an expression returning a calculated value, - // a transient (i.e. a copy) so this statement has no effect + // a transient (i.e. a copy), so this statement has no effect // except waste a lot of time cloning -array[0].update(); // <- call this method-call style will update 'a' +array[0].update(); // <- call in method-call style will update 'a' ``` diff --git a/doc/src/language/modules/ast.md b/doc/src/language/modules/ast.md index 96607881..4bf39ace 100644 --- a/doc/src/language/modules/ast.md +++ b/doc/src/language/modules/ast.md @@ -3,7 +3,7 @@ Create a Module from an AST {{#include ../../links.md}} -It is easy to convert a pre-compiled `AST` into a module: just use `Module::eval_ast_as_new`. +It is easy to convert a pre-compiled [`AST`] into a module: just use `Module::eval_ast_as_new`. Don't forget the [`export`] statement, otherwise there will be no variables exposed by the module other than non-[`private`] functions (unless that's intentional). diff --git a/doc/src/language/modules/export.md b/doc/src/language/modules/export.md index 9ab2b9e3..dbb4ddca 100644 --- a/doc/src/language/modules/export.md +++ b/doc/src/language/modules/export.md @@ -3,9 +3,9 @@ Export Variables, Functions and Sub-Modules in Module {{#include ../../links.md}} -A _module_ is a single script (or pre-compiled `AST`) containing global variables, functions and sub-modules. +A _module_ is a single script (or pre-compiled [`AST`]) containing global variables, functions and sub-modules. -A module can be created from a script via the `Module::eval_ast_as_new` method. When given an `AST`, +A module can be created from a script via the `Module::eval_ast_as_new` method. When given an [`AST`], it is first evaluated, then the following items are exposed as members of the new module: * Global variables - essentially all variables that remain in the [`Scope`] at the end of a script run - that are exported. Variables not exported (via the `export` statement) remain hidden. diff --git a/doc/src/language/modules/imp-resolver.md b/doc/src/language/modules/imp-resolver.md index 15de7253..415abf72 100644 --- a/doc/src/language/modules/imp-resolver.md +++ b/doc/src/language/modules/imp-resolver.md @@ -7,8 +7,8 @@ For many applications in which Rhai is embedded, it is necessary to customize th are resolved. For instance, modules may need to be loaded from script texts stored in a database, not in the file system. -A module resolver must implement the trait `rhai::ModuleResolver`, which contains only one function: -`resolve`. +A module resolver must implement the trait [`rhai::ModuleResolver`][traits], +which contains only one function: `resolve`. When Rhai prepares to load a module, `ModuleResolver::resolve` is called with the name of the _module path_ (i.e. the path specified in the [`import`] statement). Upon success, it should @@ -43,18 +43,14 @@ impl ModuleResolver for MyModuleResolver { } } -fn main() -> Result<(), Box> { - let mut engine = Engine::new(); +let mut engine = Engine::new(); - // Set the custom module resolver into the 'Engine'. - engine.set_module_resolver(Some(MyModuleResolver {})); +// Set the custom module resolver into the 'Engine'. +engine.set_module_resolver(Some(MyModuleResolver {})); - engine.consume(r#" - import "hello" as foo; // this 'import' statement will call - // 'MyModuleResolver::resolve' with "hello" as path - foo:bar(); - "#)?; - - Ok(()) -} +engine.consume(r#" + import "hello" as foo; // this 'import' statement will call + // 'MyModuleResolver::resolve' with "hello" as path + foo:bar(); +"#)?; ``` diff --git a/doc/src/language/modules/import.md b/doc/src/language/modules/import.md index 536e0745..4ede1f52 100644 --- a/doc/src/language/modules/import.md +++ b/doc/src/language/modules/import.md @@ -3,6 +3,9 @@ Import a Module {{#include ../../links.md}} +`import` Statement +----------------- + A module can be _imported_ via the `import` statement, and its members are accessed via '`::`' similar to C++. ```rust @@ -17,10 +20,14 @@ print(lock::status); // module variables are constants lock::status = "off"; // <- runtime error - cannot modify a constant ``` + +Scoped Imports +-------------- + `import` statements are _scoped_, meaning that they are only accessible inside the scope that they're imported. They can appear anywhere a normal statement can be, but in the vast majority of cases `import` statements are -group at the beginning of a script. It is, however, not advised to deviate from this common practice unless +group at the beginning of a script. It is not advised to deviate from this common practice unless there is a _Very Good Reason™_. Especially, do not place an `import` statement within a loop; doing so will repeatedly re-load the same module @@ -44,3 +51,32 @@ for x in range(0, 1000) { c.encrypt(something); } ``` + + +Recursive Imports +---------------- + +Beware of _import cycles_ - i.e. recursively loading the same module. This is a sure-fire way to +cause a stack overflow in the [`Engine`], unless stopped by setting a limit for [maximum number of modules]. + +For instance, importing itself always causes an infinite recursion: + +```rust +// This file is 'hello.rhai' + +import "hello" as foo; // import itself - infinite recursion! + +foo::do_something(); +``` + +Modules cross-referencing also cause infinite recursion: + +```rust +// This file is 'hello.rhai' - references 'world.rhai' +import "world" as foo; +foo::do_something(); + +// This file is 'world.rhai' - references 'hello.rhai' +import "hello" as bar; +bar::do_something_else(); +``` diff --git a/doc/src/language/modules.md b/doc/src/language/modules/index.md similarity index 96% rename from doc/src/language/modules.md rename to doc/src/language/modules/index.md index 6b616103..cac54f3d 100644 --- a/doc/src/language/modules.md +++ b/doc/src/language/modules/index.md @@ -1,7 +1,7 @@ Modules ======= -{{#include ../links.md}} +{{#include ../../links.md}} Rhai allows organizing code (functions, both Rust-based or script-based, and variables) into _modules_. Modules can be disabled via the [`no_module`] feature. diff --git a/doc/src/language/modules/resolvers.md b/doc/src/language/modules/resolvers.md index 3377cb56..ed2bf54c 100644 --- a/doc/src/language/modules/resolvers.md +++ b/doc/src/language/modules/resolvers.md @@ -5,7 +5,7 @@ Module Resolvers When encountering an [`import`] statement, Rhai attempts to _resolve_ the module based on the path string. -_Module Resolvers_ are service types that implement the [`ModuleResolver`]({{rootUrl}}/rust/traits.md) trait. +_Module Resolvers_ are service types that implement the [`ModuleResolver`][traits] trait. There are a number of standard resolvers built into Rhai, the default being the `FileModuleResolver` which simply loads a script file based on the path (with `.rhai` extension attached) and execute it to form a module. diff --git a/doc/src/language/num-fn.md b/doc/src/language/num-fn.md index c6df7900..a963c7e2 100644 --- a/doc/src/language/num-fn.md +++ b/doc/src/language/num-fn.md @@ -6,18 +6,19 @@ Numeric Functions Integer Functions ---------------- -The following standard functions (defined in the [`BasicMathPackage`]({{rootUrl}}/rust/packages.md) but excluded if using a [raw `Engine`]) +The following standard functions (defined in the [`BasicMathPackage`][packages] but excluded if using a [raw `Engine`]) operate on `i8`, `i16`, `i32`, `i64`, `f32` and `f64` only: -| Function | Description | -| ------------ | --------------------------------- | -| `abs` | absolute value | -| [`to_float`] | converts an integer type to `f64` | +| Function | Description | +| ------------ | --------------------------------------------------------------- | +| `abs` | absolute value | +| `sign` | returns -1 if the number is negative, +1 if positive, 0 if zero | +| [`to_float`] | converts an integer type to `f64` | Floating-Point Functions ----------------------- -The following standard functions (defined in the [`BasicMathPackage`]({{rootUrl}}/rust/packages.md) but excluded if using a [raw `Engine`]) +The following standard functions (defined in the [`BasicMathPackage`][packages] but excluded if using a [raw `Engine`]) operate on `f64` only: | Category | Functions | diff --git a/doc/src/language/num-op.md b/doc/src/language/num-op.md index a7bd524d..fa96f1e9 100644 --- a/doc/src/language/num-op.md +++ b/doc/src/language/num-op.md @@ -22,19 +22,19 @@ number = -5 - +5; Binary Operators ---------------- -| Operator | Description | Integers only | -| -------- | ---------------------------------------------------- | :-----------: | -| `+` | Plus | | -| `-` | Minus | | -| `*` | Multiply | | -| `/` | Divide (integer division if acting on integer types) | | -| `%` | Modulo (remainder) | | -| `~` | Power | | -| `&` | Binary _And_ bit-mask | Yes | -| `|` | Binary _Or_ bit-mask | Yes | -| `^` | Binary _Xor_ bit-mask | Yes | -| `<<` | Left bit-shift | Yes | -| `>>` | Right bit-shift | Yes | +| Operator | Description | Integers only | +| --------------- | ---------------------------------------------------- | :-----------: | +| `+` | Plus | | +| `-` | Minus | | +| `*` | Multiply | | +| `/` | Divide (integer division if acting on integer types) | | +| `%` | Modulo (remainder) | | +| `~` | Power | | +| `&` | Bit-wise _And_ | Yes | +| \| | Bit-wise _Or_ | Yes | +| `^` | Bit-wise _Xor_ | Yes | +| `<<` | Left bit-shift | Yes | +| `>>` | Right bit-shift | Yes | ```rust let x = (1 + 2) * (6 - 4) / 2; // arithmetic, with parentheses diff --git a/doc/src/language/object-maps-oop.md b/doc/src/language/object-maps-oop.md new file mode 100644 index 00000000..4727871b --- /dev/null +++ b/doc/src/language/object-maps-oop.md @@ -0,0 +1,36 @@ +Special Support for OOP via Object Maps +====================================== + +{{#include ../links.md}} + +[Object maps] can be used to simulate [object-oriented programming (OOP)][OOP] by storing data +as properties and methods as properties holding [function pointers]. + +If an [object map]'s property holds a [function pointer], the property can simply be called like +a normal method in method-call syntax. This is a _short-hand_ to avoid the more verbose syntax +of using the `call` function keyword. + +When a property holding a [function pointer] is called like a method, what happens next depends +on whether the target function is a native Rust function or a script-defined function. + +If it is a registered native Rust method function, then it is called directly. + +If it is a script-defined function, the `this` variable within the function body is bound +to the [object map] before the function is called. There is no way to simulate this behavior +via a normal function-call syntax because all scripted function arguments are passed by value. + +```rust +fn do_action(x) { print(this.data + x); } // 'this' binds to the object when called + +let obj = #{ + data: 40, + action: Fn("do_action") // 'action' holds a function pointer to 'do_action' + }; + +obj.action(2); // Short-hand syntax: prints 42 + +// To achieve the above with normal function pointer calls: +fn do_action(map, x) { print(map.data + x); } + +obj.action.call(obj, 2); // this call cannot mutate 'obj' +``` diff --git a/doc/src/language/object-maps.md b/doc/src/language/object-maps.md index 16d6b6f0..27220134 100644 --- a/doc/src/language/object-maps.md +++ b/doc/src/language/object-maps.md @@ -39,7 +39,7 @@ The index notation allows setting/getting properties of arbitrary names (even th Built-in Functions ----------------- -The following methods (defined in the [`BasicMapPackage`]({{rootUrl}}/rust/packages.md) but excluded if using a [raw `Engine`]) +The following methods (defined in the [`BasicMapPackage`][packages] but excluded if using a [raw `Engine`]) operate on object maps: | Function | Parameter(s) | Description | @@ -50,6 +50,7 @@ operate on object maps: | `remove` | property name | removes a certain property and returns it ([`()`] if the property does not exist) | | `+=` operator, `mixin` | second object map | mixes in all the properties of the second object map to the first (values of properties with the same names replace the existing values) | | `+` operator | first object map, second object map | merges the first object map with the second | +| `fill_with` | second object map | adds in all properties of the second object map that do not exist in the object map | | `keys` | _none_ | returns an [array] of all the property names (in random order), not available under [`no_index`] | | `values` | _none_ | returns an [array] of all the property values (in random order), not available under [`no_index`] | @@ -61,7 +62,7 @@ Examples let y = #{ // object map literal with 3 properties a: 1, bar: "hello", - "baz!$@": 123.456, // like JS, you can use any string as property names... + "baz!$@": 123.456, // like JavaScript, you can use any string as property names... "": false, // even the empty string! a: 42 // <- syntax error: duplicated property name diff --git a/doc/src/language/oop.md b/doc/src/language/oop.md new file mode 100644 index 00000000..120b42d0 --- /dev/null +++ b/doc/src/language/oop.md @@ -0,0 +1,44 @@ +Object-Oriented Programming (OOP) +================================ + +{{#include ../links.md}} + +Rhai does not have _objects_ per se, but it is possible to _simulate_ object-oriented programming. + + +Use [Object Maps] to Simulate OOP +-------------------------------- + +Rhai's [object maps] has [special support for OOP]({{rootUrl}}/language/object-maps-oop.md). + +| Rhai concept | Maps to OOP | +| ----------------------------------------------------- | :---------: | +| [Object maps] | objects | +| [Object map] properties holding values | properties | +| [Object map] properties that hold [function pointers] | methods | + + +Examples +-------- + +```rust +// Define the object +let obj = #{ + data: 0, + increment: Fn("add"), // when called, 'this' binds to 'obj' + update: Fn("update"), // when called, 'this' binds to 'obj' + action: Fn("action") // when called, 'this' binds to 'obj' + }; + +// Define functions +fn add(x) { this.data += x; } // update using 'this' +fn update(x) { this.data = x; } // update using 'this' +fn action() { print(this.data); } // access properties of 'this' + +// Use the object +obj.increment(1); +obj.action(); // prints 1 + +obj.update(42); +obj.action(); // prints 42 +``` diff --git a/doc/src/language/overload.md b/doc/src/language/overload.md index b83a8028..bb291ca3 100644 --- a/doc/src/language/overload.md +++ b/doc/src/language/overload.md @@ -3,20 +3,27 @@ Function Overloading {{#include ../links.md}} -Functions defined in script can be _overloaded_ by _arity_ (i.e. they are resolved purely upon the function's _name_ +[Functions] defined in script can be _overloaded_ by _arity_ (i.e. they are resolved purely upon the function's _name_ and _number_ of parameters, but not parameter _types_ since all parameters are the same type - [`Dynamic`]). New definitions _overwrite_ previous definitions of the same name and number of parameters. ```rust fn foo(x,y,z) { print("Three!!! " + x + "," + y + "," + z) } + fn foo(x) { print("One! " + x) } + fn foo(x,y) { print("Two! " + x + "," + y) } + fn foo() { print("None.") } + fn foo(x) { print("HA! NEW ONE! " + x) } // overwrites previous definition foo(1,2,3); // prints "Three!!! 1,2,3" + foo(42); // prints "HA! NEW ONE! 42" + foo(1,2); // prints "Two!! 1,2" + foo(); // prints "None." ``` diff --git a/doc/src/language/print-debug.md b/doc/src/language/print-debug.md index bd031b4a..515b098f 100644 --- a/doc/src/language/print-debug.md +++ b/doc/src/language/print-debug.md @@ -7,8 +7,11 @@ The `print` and `debug` functions default to printing to `stdout`, with `debug` ```rust print("hello"); // prints hello to stdout + print(1 + 2 + 3); // prints 6 to stdout + print("hello" + 42); // prints hello42 to stdout + debug("world!"); // prints "world!" to stdout using debug formatting ``` diff --git a/doc/src/language/return.md b/doc/src/language/return.md index 5e696161..41c063f9 100644 --- a/doc/src/language/return.md +++ b/doc/src/language/return.md @@ -3,8 +3,14 @@ Return Values {{#include ../links.md}} +The `return` statement is used to immediately stop evaluation and exist the current context +(typically a function call) yielding a _return value_. + ```rust return; // equivalent to return (); return 123 + 456; // returns 579 ``` + +A `return` statement at _global_ level stop the entire script evaluation, +the return value is taken as the result of the script evaluation. diff --git a/doc/src/language/statements.md b/doc/src/language/statements.md index c964d952..450011c2 100644 --- a/doc/src/language/statements.md +++ b/doc/src/language/statements.md @@ -3,13 +3,14 @@ Statements {{#include ../links.md}} +Terminated by '`;`' +------------------ + Statements are terminated by semicolons '`;`' and they are mandatory, except for the _last_ statement in a _block_ (enclosed by '`{`' .. '`}`' pairs) where it can be omitted. -A statement can be used anywhere where an expression is expected. These are called, for lack of a more -creative name, "statement expressions." The _last_ statement of a statement block is _always_ the block's -return value when used as a statement. -If the last statement has no return value (e.g. variable definitions, assignments) then it is assumed to be [`()`]. +Semicolons can also be omitted if the statement contains a block itself +(e.g. the `if`, `while`, `for` and `loop` statements). ```rust let a = 42; // normal assignment statement @@ -20,6 +21,20 @@ let a = { 40 + 2 }; // 'a' is set to the value of the statement block, which // ^ the last statement does not require a terminating semicolon (although it also works with it) // ^ semicolon required here to terminate the assignment statement; it is a syntax error without it -4 * 10 + 2 // a statement which is just one expression; no ending semicolon is OK +if foo { a = 42 } +// ^ there is no need to terminate an if-statement with a semicolon + +4 * 10 + 2 // a statement which is just one expression - no ending semicolon is OK // because it is the last statement of the whole block ``` + + +Statement Expression +-------------------- + +A statement can be used anywhere where an expression is expected. These are called, for lack of a more +creative name, "statement expressions." + +The _last_ statement of a statement block is _always_ the block's return value when used as a statement. + +If the last statement has no return value (e.g. variable definitions, assignments) then it is assumed to be [`()`]. diff --git a/doc/src/language/string-fn.md b/doc/src/language/string-fn.md index 8504e2e3..504ea0a6 100644 --- a/doc/src/language/string-fn.md +++ b/doc/src/language/string-fn.md @@ -3,7 +3,7 @@ Built-in String Functions {{#include ../links.md}} -The following standard methods (mostly defined in the [`MoreStringPackage`]({{rootUrl}}/rust/packages.md) but excluded if +The following standard methods (mostly defined in the [`MoreStringPackage`][packages] but excluded if using a [raw `Engine`]) operate on [strings]: | Function | Parameter(s) | Description | diff --git a/doc/src/language/strings-chars.md b/doc/src/language/strings-chars.md index f7019048..b6372240 100644 --- a/doc/src/language/strings-chars.md +++ b/doc/src/language/strings-chars.md @@ -6,7 +6,7 @@ Strings and Characters String in Rhai contain any text sequence of valid Unicode characters. Internally strings are stored in UTF-8 encoding. -Strings can be built up from other strings and types via the `+` operator (provided by the [`MoreStringPackage`]({{rootUrl}}/rust/packages.md) +Strings can be built up from other strings and types via the `+` operator (provided by the [`MoreStringPackage`][packages] but excluded if using a [raw `Engine`]). This is particularly useful when printing output. [`type_of()`] a string returns `"string"`. diff --git a/doc/src/language/timestamps.md b/doc/src/language/timestamps.md index 3d02797e..47ba302b 100644 --- a/doc/src/language/timestamps.md +++ b/doc/src/language/timestamps.md @@ -3,12 +3,12 @@ {{#include ../links.md}} -Timestamps are provided by the [`BasicTimePackage`]({{rootUrl}}/rust/packages.md) (excluded if using a [raw `Engine`]) +Timestamps are provided by the [`BasicTimePackage`][packages] (excluded if using a [raw `Engine`]) via the `timestamp` function. Timestamps are not available under [`no_std`]. -The Rust type of a timestamp is `std::time::Instant` ([`instant::Instant`](https://crates.io/crates/instant) in [WASM] builds). +The Rust type of a timestamp is `std::time::Instant` ([`instant::Instant`] in [WASM] builds). [`type_of()`] a timestamp returns `"timestamp"`. @@ -16,7 +16,7 @@ The Rust type of a timestamp is `std::time::Instant` ([`instant::Instant`](https Built-in Functions ----------------- -The following methods (defined in the [`BasicTimePackage`]({{rootUrl}}/rust/packages.md) but excluded if using a [raw `Engine`]) operate on timestamps: +The following methods (defined in the [`BasicTimePackage`][packages] but excluded if using a [raw `Engine`]) operate on timestamps: | Function | Parameter(s) | Description | | ----------------------------- | ---------------------------------- | -------------------------------------------------------- | diff --git a/doc/src/language/values-and-types.md b/doc/src/language/values-and-types.md index 2aef02c3..70c276a9 100644 --- a/doc/src/language/values-and-types.md +++ b/doc/src/language/values-and-types.md @@ -5,20 +5,21 @@ Values and Types The following primitive types are supported natively: -| Category | Equivalent Rust types | [`type_of()`] | `to_string()` | -| --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------- | --------------------- | -| **Integer number** | `u8`, `i8`, `u16`, `i16`,
`u32`, `i32` (default for [`only_i32`]),
`u64`, `i64` _(default)_ | `"i32"`, `"u64"` etc. | `"42"`, `"123"` etc. | -| **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`) | `"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`]({{rootUrl}}/rust/packages.md), disabled with [`no_std`]) | `std::time::Instant` ([instant::Instant](https://crates.io/crates/instant) if not [WASM] build) | `"timestamp"` | _not supported_ | -| **Dynamic value** (i.e. can be anything) | `rhai::Dynamic` | _the actual type_ | _actual value_ | -| **System integer** (current configuration) | `rhai::INT` (`i32` or `i64`) | `"i32"` or `"i64"` | `"42"`, `"123"` etc. | -| **System floating-point** (current configuration, disabled with [`no_float`]) | `rhai::FLOAT` (`f32` or `f64`) | `"f32"` or `"f64"` | `"123.456"` etc. | -| **Nothing/void/nil/null/Unit** (or whatever it is called) | `()` | `"()"` | `""` _(empty string)_ | +| Category | Equivalent Rust types | [`type_of()`] | `to_string()` | +| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------- | ----------------------- | +| **Integer number** | `u8`, `i8`, `u16`, `i16`,
`u32`, `i32` (default for [`only_i32`]),
`u64`, `i64` _(default)_ | `"i32"`, `"u64"` etc. | `"42"`, `"123"` etc. | +| **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`) | `"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], disabled with [`no_std`]) | `std::time::Instant` ([`instant::Instant`] if not [WASM] build) | `"timestamp"` | _not supported_ | +| **[Function pointer]** | _None_ | `Fn` | `"Fn(foo)"` | +| **[`Dynamic`] value** (i.e. can be anything) | `rhai::Dynamic` | _the actual type_ | _actual value_ | +| **System integer** (current configuration) | `rhai::INT` (`i32` or `i64`) | `"i32"` or `"i64"` | `"42"`, `"123"` etc. | +| **System floating-point** (current configuration, disabled with [`no_float`]) | `rhai::FLOAT` (`f32` or `f64`) | `"f32"` or `"f64"` | `"123.456"` etc. | +| **Nothing/void/nil/null/Unit** (or whatever it is called) | `()` | `"()"` | `""` _(empty string)_ | All types are treated strictly separate by Rhai, meaning that `i32` and `i64` and `u32` are completely different - they even cannot be added together. This is very similar to Rust. diff --git a/doc/src/language/variables.md b/doc/src/language/variables.md index 94f0500b..be4abbce 100644 --- a/doc/src/language/variables.md +++ b/doc/src/language/variables.md @@ -3,17 +3,37 @@ Variables {{#include ../links.md}} -Variables in Rhai follow normal C naming rules (i.e. must contain only ASCII letters, digits and underscores '`_`'). +Valid Names +----------- -Variable names must start with an ASCII letter or an underscore '`_`', must contain at least one ASCII letter, -and must start with an ASCII letter before a digit. +Variables in Rhai follow normal C naming rules - must contain only ASCII letters, digits and underscores '`_`', +and cannot start with a digit. -Therefore, names like '`_`', '`_42`', '`3a`' etc. are not legal variable names, but '`_c3po`' and '`r2d2`' are. -Variable names are also case _sensitive_. +For example: '`_c3po`' and '`r2d2`' are valid variable names, but '`3abc`' is not. -Variables are defined using the `let` keyword. A variable defined within a statement block is _local_ to that block. +However, unlike Rust, a variable name must also contain at least one ASCII letter, and an ASCII letter must come before any digit. +In other words, the first character that is not an underscore '`_`' must be an ASCII letter and not a digit. + +Therefore, some names acceptable to Rust, like '`_`', '`_42foo`', '`_1`' etc., are not valid in Rhai. +This restriction is to reduce confusion because, for instance, '`_1`' can easily be misread (or mis-typed) as `-1`. + +Variable names are case _sensitive_. + +Variable names also cannot be the same as a [keyword]. + + +Declare a Variable +------------------ + +Variables are declared using the `let` keyword. + +Variables do not have to be given an initial value. +If none is provided, it defaults to [`()`]. + +A variable defined within a statement block is _local_ to that block. ```rust +let x; // ok - value is '()' let x = 3; // ok let _x = 42; // ok let x_ = 42; // also ok diff --git a/doc/src/language/while.md b/doc/src/language/while.md index f0f419fb..e912175e 100644 --- a/doc/src/language/while.md +++ b/doc/src/language/while.md @@ -3,6 +3,11 @@ {{#include ../links.md}} +`while` loops follow C syntax. + +Like C, `continue` can be used to skip to the next iteration, by-passing all following statements; +`break` can be used to break out of the loop unconditionally. + ```rust let x = 10; diff --git a/doc/src/links.md b/doc/src/links.md index cc4dd9f0..b7147a50 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -10,21 +10,24 @@ [`no_function`]: {{rootUrl}}/start/features.md [`no_module`]: {{rootUrl}}/start/features.md [`no_std`]: {{rootUrl}}/start/features.md - [`no-std`]: {{rootUrl}}/start/features.md +[`internals`]: {{rootUrl}}/start/features.md [minimal builds]: {{rootUrl}}/start/builds/minimal.md [WASM]: {{rootUrl}}/start/builds/wasm.md +[playground]: https://alvinhochun.github.io/rhai-demo [`Engine`]: {{rootUrl}}/engine/hello-world.md +[traits]: {{rootUrl}}/rust/traits.md [`private`]: {{rootUrl}}/engine/call-fn.md [`Func`]: {{rootUrl}}/engine/func.md +[`AST`]: {{rootUrl}}/engine/compile.md [`eval_expression`]: {{rootUrl}}/engine/expressions.md [`eval_expression_with_scope`]: {{rootUrl}}/engine/expressions.md [raw `Engine`]: {{rootUrl}}/engine/raw.md [built-in operators]: {{rootUrl}}/engine/raw.md#built-in-operators -[package]: {{rootUrl}}/rust/packages.md -[packages]: {{rootUrl}}/rust/packages.md +[package]: {{rootUrl}}/rust/packages/index.md +[packages]: {{rootUrl}}/rust/packages/index.md [`Scope`]: {{rootUrl}}/rust/scope.md [`type_of()`]: {{rootUrl}}/language/type-of.md @@ -37,10 +40,17 @@ [custom type]: {{rootUrl}}/rust/custom.md [custom types]: {{rootUrl}}/rust/custom.md +[getters/setters]: {{rootUrl}}/rust/getters-setters.md +[indexers]: {{rootUrl}}/rust/indexers.md + +[`instant::Instant`]: https://crates.io/crates/instant [`print`]: {{rootUrl}}/language/print-debug.md [`debug`]: {{rootUrl}}/language/print-debug.md +[keywords]: {{rootUrl}}/appendix/keywords.md +[keyword]: {{rootUrl}}/appendix/keywords.md + [variable]: {{rootUrl}}/language/variables.md [variables]: {{rootUrl}}/language/variables.md @@ -62,15 +72,22 @@ [function]: {{rootUrl}}/language/functions.md [functions]: {{rootUrl}}/language/functions.md +[function pointer]: {{rootUrl}}/language/fn-ptr.md +[function pointers]: {{rootUrl}}/language/fn-ptr.md +[function namespace]: {{rootUrl}}/language/fn-namespaces.md +[function namespaces]: {{rootUrl}}/language/fn-namespaces.md -[`Module`]: {{rootUrl}}/language/modules.md -[module]: {{rootUrl}}/language/modules.md -[modules]: {{rootUrl}}/language/modules.md +[`Module`]: {{rootUrl}}/language/modules/index.md +[module]: {{rootUrl}}/language/modules/index.md +[modules]: {{rootUrl}}/language/modules/index.md +[module resolver]: {{rootUrl}}/language/modules/imp-resolver.md [`export`]: {{rootUrl}}/language/modules/export.md [`import`]: {{rootUrl}}/language/modules/import.md [`eval`]: {{rootUrl}}/language/eval.md +[OOP]: {{rootUrl}}/language/oop.md + [maximum statement depth]: {{rootUrl}}/safety/max-stmt-depth.md [maximum call stack depth]: {{rootUrl}}/safety/max-call-stack.md [maximum number of operations]: {{rootUrl}}/safety/max-operations.md @@ -78,9 +95,9 @@ [maximum length of strings]: {{rootUrl}}/safety/max-string-size.md [maximum size of arrays]: {{rootUrl}}/safety/max-array-size.md [maximum size of object maps]: {{rootUrl}}/safety/max-map-size.md -[progress]:/safety/progress.md +[progress]: {{rootUrl}}/safety/progress.md -[script optimization]: {{rootUrl}}/engine/optimize.md +[script optimization]: {{rootUrl}}/engine/optimize/index.md [`OptimizationLevel::Full`]: {{rootUrl}}/engine/optimize/optimize-levels.md [`OptimizationLevel::Simple`]: {{rootUrl}}/engine/optimize/optimize-levels.md [`OptimizationLevel::None`]: {{rootUrl}}/engine/optimize/optimize-levels.md diff --git a/doc/src/rust/builtin-packages.md b/doc/src/rust/builtin-packages.md deleted file mode 100644 index f77cc900..00000000 --- a/doc/src/rust/builtin-packages.md +++ /dev/null @@ -1 +0,0 @@ -# Built-in Packages diff --git a/doc/src/rust/custom.md b/doc/src/rust/custom.md index cd3998b6..b87f6409 100644 --- a/doc/src/rust/custom.md +++ b/doc/src/rust/custom.md @@ -26,21 +26,16 @@ impl TestStruct { } } -fn main() -> Result<(), Box> -{ - let engine = Engine::new(); +let mut engine = Engine::new(); - engine.register_type::(); +engine.register_type::(); - engine.register_fn("update", TestStruct::update); - engine.register_fn("new_ts", TestStruct::new); +engine.register_fn("update", TestStruct::update); +engine.register_fn("new_ts", TestStruct::new); - let result = engine.eval::("let x = new_ts(); x.update(); x")?; +let result = engine.eval::("let x = new_ts(); x.update(); x")?; - println!("result: {}", result.field); // prints 42 - - Ok(()) -} +println!("result: {}", result.field); // prints 42 ``` Register a Custom Type @@ -66,7 +61,7 @@ impl TestStruct { } } -let engine = Engine::new(); +let mut engine = Engine::new(); engine.register_type::(); ``` @@ -102,15 +97,17 @@ println!("result: {}", result.field); // prints 42 Method-Call Style vs. Function-Call Style ---------------------------------------- -In fact, any function with a first argument that is a `&mut` reference can be used as method calls because -internally they are the same thing: methods on a type is implemented as a functions taking a `&mut` first argument. +Any function with a first argument that is a `&mut` reference can be used +as method calls because internally they are the same thing: methods on a type is +implemented as a functions taking a `&mut` first argument. +This design is similar to Rust. ```rust fn foo(ts: &mut TestStruct) -> i64 { ts.field } -engine.register_fn("foo", foo); // register ad hoc function with correct signature +engine.register_fn("foo", foo); // register a Rust native function let result = engine.eval::( "let x = new_ts(); x.foo()" // 'foo' can be called like a method on 'x' @@ -119,8 +116,8 @@ let result = engine.eval::( println!("result: {}", result); // prints 1 ``` -Under [`no_object`], 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. diff --git a/doc/src/rust/fallible.md b/doc/src/rust/fallible.md index 6c20bf94..16c16826 100644 --- a/doc/src/rust/fallible.md +++ b/doc/src/rust/fallible.md @@ -8,11 +8,6 @@ If a function is _fallible_ (i.e. it returns a `Result<_, Error>`), it can be re The function must return `Result>`. -`Box` implements `From<&str>` and `From` etc. -and the error text gets converted into `Box`. - -The error values are `Box`-ed in order to reduce memory footprint of the error path, which should be hit rarely. - ```rust use rhai::{Engine, EvalAltResult, Position}; use rhai::RegisterResultFn; // use 'RegisterResultFn' trait for 'register_result_fn' @@ -27,15 +22,20 @@ fn safe_divide(x: i64, y: i64) -> Result> { } } -fn main() -{ - let engine = Engine::new(); +let mut engine = Engine::new(); - // Fallible functions that return Result values must use register_result_fn() - engine.register_result_fn("divide", safe_divide); +// Fallible functions that return Result values must use register_result_fn() +engine.register_result_fn("divide", safe_divide); - if let Err(error) = engine.eval::("divide(40, 0)") { - println!("Error: {:?}", *error); // prints ErrorRuntime("Division by zero detected!", (1, 1)") - } +if let Err(error) = engine.eval::("divide(40, 0)") { + println!("Error: {:?}", *error); // prints ErrorRuntime("Division by zero detected!", (1, 1)") } ``` + +Create a `Box` +---------------------------- + +`Box` implements `From<&str>` and `From` etc. +and the error text gets converted into `Box`. + +The error values are `Box`-ed in order to reduce memory footprint of the error path, which should be hit rarely. diff --git a/doc/src/rust/functions.md b/doc/src/rust/functions.md index de60646f..aca6fe8e 100644 --- a/doc/src/rust/functions.md +++ b/doc/src/rust/functions.md @@ -29,30 +29,25 @@ fn get_any_value() -> Result> { Ok((42_i64).into()) // standard types can use 'into()' } -fn main() -> Result<(), Box> -{ - let engine = Engine::new(); +let mut engine = Engine::new(); - engine.register_fn("add", add_len); - engine.register_fn("add_str", add_len_str); +engine.register_fn("add", add_len); +engine.register_fn("add_str", add_len_str); - let result = engine.eval::(r#"add(40, "xx")"#)?; +let result = engine.eval::(r#"add(40, "xx")"#)?; - println!("Answer: {}", result); // prints 42 +println!("Answer: {}", result); // prints 42 - let result = engine.eval::(r#"add_str(40, "xx")"#)?; +let result = engine.eval::(r#"add_str(40, "xx")"#)?; - println!("Answer: {}", result); // prints 42 +println!("Answer: {}", result); // prints 42 - // Functions that return Dynamic values must use register_result_fn() - engine.register_result_fn("get_any_value", get_any_value); +// Functions that return Dynamic values must use register_result_fn() +engine.register_result_fn("get_any_value", get_any_value); - let result = engine.eval::("get_any_value()")?; +let result = engine.eval::("get_any_value()")?; - println!("Answer: {}", result); // prints 42 - - Ok(()) -} +println!("Answer: {}", result); // prints 42 ``` To create a [`Dynamic`] value, use the `Dynamic::from` method. diff --git a/doc/src/rust/generic.md b/doc/src/rust/generic.md index 84527951..c68bf562 100644 --- a/doc/src/rust/generic.md +++ b/doc/src/rust/generic.md @@ -17,14 +17,11 @@ fn show_it(x: &mut T) { println!("put up a good show: {}!", x) } -fn main() -{ - let engine = Engine::new(); +let mut engine = Engine::new(); - engine.register_fn("print", show_it::); - engine.register_fn("print", show_it::); - engine.register_fn("print", show_it::); -} +engine.register_fn("print", show_it::); +engine.register_fn("print", show_it::); +engine.register_fn("print", show_it::); ``` The above example shows how to register multiple functions diff --git a/doc/src/rust/getters-setters.md b/doc/src/rust/getters-setters.md index 18cf868e..92c659b9 100644 --- a/doc/src/rust/getters-setters.md +++ b/doc/src/rust/getters-setters.md @@ -28,7 +28,7 @@ impl TestStruct { } } -let engine = Engine::new(); +let mut engine = Engine::new(); engine.register_type::(); diff --git a/doc/src/rust.md b/doc/src/rust/index.md similarity index 91% rename from doc/src/rust.md rename to doc/src/rust/index.md index bad92288..6a0ca08d 100644 --- a/doc/src/rust.md +++ b/doc/src/rust/index.md @@ -1,7 +1,7 @@ Extend Rhai with Rust ==================== -{{#include links.md}} +{{#include ../links.md}} Most features and functionalities required by a Rhai script should actually be coded in Rust, which leverages the superior native run-time speed. diff --git a/doc/src/rust/indexers.md b/doc/src/rust/indexers.md index 9c85e9a5..800c43b8 100644 --- a/doc/src/rust/indexers.md +++ b/doc/src/rust/indexers.md @@ -9,6 +9,9 @@ A custom type with an indexer function defined can use the bracket '`[]`' notati Indexers are disabled when the [`no_index`] feature is used. +For efficiency reasons, indexers **cannot** be used to overload (i.e. override) built-in indexing operations for +[arrays] and [object maps]. + ```rust #[derive(Clone)] struct TestStruct { @@ -28,7 +31,7 @@ impl TestStruct { } } -let engine = Engine::new(); +let mut engine = Engine::new(); engine.register_type::(); @@ -42,6 +45,3 @@ let result = engine.eval::("let a = new_ts(); a[2] = 42; a[2]")?; println!("Answer: {}", result); // prints 42 ``` - -For efficiency reasons, indexers **cannot** be used to overload (i.e. override) built-in indexing operations for -[arrays] and [object maps]. diff --git a/doc/src/rust/operators.md b/doc/src/rust/operators.md index b91b1cdc..8374bfb4 100644 --- a/doc/src/rust/operators.md +++ b/doc/src/rust/operators.md @@ -17,6 +17,10 @@ Similarly, comparison operators including `==`, `!=` etc. are all implemented as with the stark exception of `&&` and `||`. Because they [_short-circuit_]({{rootUrl}}/language/logic.md#boolean-operators), `&&` and `||` are handled specially and _not_ via a function; as a result, overriding them has no effect at all. + +Overload Operator via Rust Function +---------------------------------- + Operator functions cannot be defined as a script function (because operators syntax are not valid function names). However, operator functions _can_ be registered to the [`Engine`] via the methods @@ -48,6 +52,10 @@ engine.register_fn("+", mixed_add); // register '+' operator for let result: i64 = engine.eval("1 + 1.0"); // prints 2.0 (normally an error) ``` + +Considerations +-------------- + Normally, use operator overloading for [custom types] only. Be very careful when overriding built-in operators because script authors expect standard operators to behave in a diff --git a/doc/src/rust/packages/builtin.md b/doc/src/rust/packages/builtin.md index da33bfe4..4b4fba81 100644 --- a/doc/src/rust/packages/builtin.md +++ b/doc/src/rust/packages/builtin.md @@ -18,6 +18,7 @@ Built-In Packages | `BasicMathPackage` | Basic math functions (e.g. `sin`, `sqrt`) | No | Yes | | `BasicArrayPackage` | Basic [array] functions (not available under `no_index`) | No | Yes | | `BasicMapPackage` | Basic [object map] functions (not available under `no_object`) | No | Yes | +| `BasicFnPackage` | Basic methods for [function pointers]. | Yes | Yes | | `EvalPackage` | Disable [`eval`] | No | No | | `CorePackage` | Basic essentials | Yes | Yes | | `StandardPackage` | Standard library (default for `Engine::new`) | No | Yes | diff --git a/doc/src/rust/packages.md b/doc/src/rust/packages/index.md similarity index 98% rename from doc/src/rust/packages.md rename to doc/src/rust/packages/index.md index 6f8c5b6c..0b41656f 100644 --- a/doc/src/rust/packages.md +++ b/doc/src/rust/packages/index.md @@ -1,7 +1,7 @@ Packages ======== -{{#include ../links.md}} +{{#include ../../links.md}} Standard built-in Rhai features are provided in various _packages_ that can be loaded via a call to `Engine::load_package`. diff --git a/doc/src/rust/print-custom.md b/doc/src/rust/print-custom.md index 53996fe0..c729c49d 100644 --- a/doc/src/rust/print-custom.md +++ b/doc/src/rust/print-custom.md @@ -3,14 +3,15 @@ Printing for Custom Types {{#include ../links.md}} -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`): +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 | +| `print` | \|s: &mut T\| -> ImmutableString | `s.to_string().into()` | Converts the custom type into a [string] for the [`print`] statement | +| `debug` | \|s: &mut T\| -> ImmutableString | `format!("{:?}", s).into()` | Converts the custom type into a [string] for the [`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 | diff --git a/doc/src/rust/scope.md b/doc/src/rust/scope.md index d5ad8fa5..a33e4b4c 100644 --- a/doc/src/rust/scope.md +++ b/doc/src/rust/scope.md @@ -20,40 +20,35 @@ then the same state is threaded through multiple invocations: ```rust use rhai::{Engine, Scope, EvalAltResult}; -fn main() -> Result<(), Box> -{ - let engine = Engine::new(); +let engine = Engine::new(); - // First create the state - let mut scope = Scope::new(); +// First create the state +let mut scope = Scope::new(); - // Then push (i.e. add) some initialized variables into the state. - // Remember the system number types in Rhai are i64 (i32 if 'only_i32') ond f64. - // Better stick to them or it gets hard working with the script. - scope.push("y", 42_i64); - scope.push("z", 999_i64); +// Then push (i.e. add) some initialized variables into the state. +// Remember the system number types in Rhai are i64 (i32 if 'only_i32') ond f64. +// Better stick to them or it gets hard working with the script. +scope.push("y", 42_i64); +scope.push("z", 999_i64); - // 'set_value' adds a variable when one doesn't exist - scope.set_value("s", "hello, world!".to_string()); // remember to use 'String', not '&str' +// 'set_value' adds a variable when one doesn't exist +scope.set_value("s", "hello, world!".to_string()); // remember to use 'String', not '&str' - // First invocation - engine.eval_with_scope::<()>(&mut scope, r" - let x = 4 + 5 - y + z + s.len; - y = 1; - ")?; +// First invocation +engine.eval_with_scope::<()>(&mut scope, r" + let x = 4 + 5 - y + z + s.len; + y = 1; +")?; - // Second invocation using the same state - let result = engine.eval_with_scope::(&mut scope, "x")?; +// Second invocation using the same state +let result = engine.eval_with_scope::(&mut scope, "x")?; - println!("result: {}", result); // prints 979 +println!("result: {}", result); // prints 979 - // Variable y is changed in the script - read it with 'get_value' - assert_eq!(scope.get_value::("y").expect("variable y should exist"), 1); +// Variable y is changed in the script - read it with 'get_value' +assert_eq!(scope.get_value::("y").expect("variable y should exist"), 1); - // We can modify scope variables directly with 'set_value' - scope.set_value("y", 42_i64); - assert_eq!(scope.get_value::("y").expect("variable y should exist"), 42); - - Ok(()) -} +// We can modify scope variables directly with 'set_value' +scope.set_value("y", 42_i64); +assert_eq!(scope.get_value::("y").expect("variable y should exist"), 42); ``` diff --git a/doc/src/safety.md b/doc/src/safety/index.md similarity index 52% rename from doc/src/safety.md rename to doc/src/safety/index.md index d207fe9d..22ae237d 100644 --- a/doc/src/safety.md +++ b/doc/src/safety/index.md @@ -1,31 +1,35 @@ Safety and Protection Against DoS Attacks ======================================== -{{#include links.md}} +{{#include ../links.md}} 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 a [string], an [array] or [object map] until all memory is consumed. +* **Memory**: A malicious 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. +* **CPU**: A malicious 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. +* **Time**: A malicious script may run indefinitely, thereby blocking the calling system which is waiting for a result. + +* **Stack**: A malicious script may attempt an infinite recursive call that exhausts the call stack. -* **Stack**: A malicous script may attempt an infinite recursive call that exhausts the call stack. Alternatively, it may create a degenerated deep expression with so many levels that the parser exhausts the call stack when parsing the expression; or even deeply-nested statement blocks, if nested deep enough. -* **Overflows**: A malicous script may deliberately cause numeric over-flows and/or under-flows, divide by zero, and/or + Another way to cause a stack overflow is to load a [self-referencing module][`import`]. + +* **Overflows**: A malicious script may deliberately cause numeric over-flows and/or under-flows, divide by zero, and/or create bad floating-point representations, in order to crash the system. -* **Files**: A malicous script may continuously [`import`] an external module within an infinite loop, +* **Files**: A malicious script may continuously [`import`] an external module within an infinite loop, thereby putting heavy load on the file-system (or even the network if the file is not local). - Furthermore, the module script may simply [`import`] itself in an infinite recursion. + Even when modules are not created from files, they still typically consume a lot of resources to load. -* **Data**: A malicous script may attempt to read from and/or write to data that it does not own. If this happens, +* **Data**: A malicious 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. diff --git a/doc/src/safety/max-modules.md b/doc/src/safety/max-modules.md index d6f2f8da..adb1c133 100644 --- a/doc/src/safety/max-modules.md +++ b/doc/src/safety/max-modules.md @@ -10,6 +10,8 @@ of modules to zero does _not_ indicate unlimited modules, but disallows loading A script attempting to load more than the maximum number of modules will terminate with an error result. +This limit can also be used to stop [`import`-loops][`import`] (i.e. cycles of modules referring to each other). + This check can be disabled via the [`unchecked`] feature for higher performance (but higher risks as well). diff --git a/doc/src/safety/max-stmt-depth.md b/doc/src/safety/max-stmt-depth.md index 007e7c82..e8b5ee84 100644 --- a/doc/src/safety/max-stmt-depth.md +++ b/doc/src/safety/max-stmt-depth.md @@ -25,7 +25,7 @@ This limit may be changed via the `Engine::set_max_expr_depths` method. There are two limits to set, one for the maximum depth at global level, and the other for function bodies. A script exceeding the maximum nesting depths will terminate with a parsing error. -The malicious `AST` will not be able to get past parsing in the first place. +The malicious [`AST`] will not be able to get past parsing in the first place. This check can be disabled via the [`unchecked`] feature for higher performance (but higher risks as well). diff --git a/doc/src/start/builds.md b/doc/src/start/builds/index.md similarity index 86% rename from doc/src/start/builds.md rename to doc/src/start/builds/index.md index ac2023d5..a3cdfbf3 100644 --- a/doc/src/start/builds.md +++ b/doc/src/start/builds/index.md @@ -1,7 +1,7 @@ Special Builds ============== -{{#include ../links.md}} +{{#include ../../links.md}} It is possible to mix-and-match various [features] of the Rhai crate to make specialized builds with specific characteristics and behaviors. diff --git a/doc/src/start/builds/minimal.md b/doc/src/start/builds/minimal.md index 57ad59c7..d1da0f01 100644 --- a/doc/src/start/builds/minimal.md +++ b/doc/src/start/builds/minimal.md @@ -17,6 +17,16 @@ opt-level = "z" # optimize for size ``` +Use `i32` Only +-------------- + +For embedded systems that must optimize for code size, the architecture is commonly 32-bit. +Use [`only_i32`] to prune away large sections of code implementing functions for other numeric types +(including `i64`). + +If, for some reason, 64-bit long integers must be supported, use [`only_i64`] instead of [`only_i32`]. + + Opt-Out of Features ------------------ @@ -28,13 +38,17 @@ Omitting arrays ([`no_index`]) yields the most code-size savings, followed by fl ([`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. +Disable script-defined functions ([`no_function`]) when the feature is not needed. +Both of these have little code size savings. Use a Raw [`Engine`] ------------------- -[`Engine::new_raw`](#raw-engine) creates a _raw_ engine. -A _raw_ engine supports, out of the box, only a very [restricted set](#built-in-operators) of basic arithmetic and logical operators. +[`Engine::new_raw`][raw `Engine`] creates a _raw_ engine. +A _raw_ engine supports, out of the box, only a very [restricted set]({{rootUrl}}/engine/raw.md#built-in-operators) +of basic arithmetic and logical operators. + Selectively include other necessary functionalities by loading specific [packages] to minimize the footprint. + Packages are sharable (even across threads via the [`sync`] feature), so they only have to be created once. diff --git a/doc/src/start/builds/performance.md b/doc/src/start/builds/performance.md index 65099707..5db1edc1 100644 --- a/doc/src/start/builds/performance.md +++ b/doc/src/start/builds/performance.md @@ -7,14 +7,17 @@ Use Only One Integer Type ------------------------ Some features are for performance. For example, using [`only_i32`] or [`only_i64`] disables all other integer types (such as `u16`). + If only a single integer type is needed in scripts - most of the time this is the case - it is best to avoid registering -lots of functions related to other integer types that will never be used. As a result, performance should improve. +lots of functions related to other integer types that will never be used. As a result, [`Engine`] creation will be faster +because fewer functions need to be loaded. Use Only 32-Bit Numbers ---------------------- If only 32-bit integers are needed - again, most of the time this is the case - using [`only_i32`] disables also `i64`. + On 64-bit targets this may not gain much, but on some 32-bit targets this improves performance due to 64-bit arithmetic requiring more CPU cycles to complete. @@ -24,4 +27,5 @@ Minimize Size of [`Dynamic`] Turning on [`no_float`], and [`only_i32`] makes the key [`Dynamic`] data type only 8 bytes small on 32-bit targets while normally it can be up to 16 bytes (e.g. on x86/x64 CPU's) in order to hold an `i64` or `f64`. + Making [`Dynamic`] small helps performance due to better cache efficiency. diff --git a/doc/src/start/builds/wasm.md b/doc/src/start/builds/wasm.md index ca2b0dbb..1f813ae8 100644 --- a/doc/src/start/builds/wasm.md +++ b/doc/src/start/builds/wasm.md @@ -14,8 +14,41 @@ 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. + +Size +---- + +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. + + +Speed +----- In benchmark tests, a WASM build runs scripts roughly 1.7-2.2x slower than a native optimized release build. + + +Common Features +--------------- + +Some Rhai functionalities are not necessary in a WASM environment, so the following features +are typically used for a WASM build: + +| Feature | Description | +| :-----------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`unchecked`] | When a WASM module panics, it doesn't crash the entire web app; however this also disables [maximum number of operations] and [progress] tracking so a script can still run indefinitely - the web app must terminate it itself. | +| [`only_i32`] | JavaScript has only one `number` type and we're only supporting `wasm32` here (so far). | +| [`no_module`] | A WASM module cannot load modules from the file system, so usually this is not needed, but the savings are minimal; alternatively, a custom [module resolver] can be provided that loads other Rhai scripts. | + +The following features are typically _not_ used because they don't make sense in a WASM build: + +| Feature | Why unnecessary | +| :-----------: | ------------------------------------------------------------------ | +| [`sync`] | WASM is single-threaded. | +| [`no_std`] | `std` lib works fine with WASM. | +| [`internals`] | WASM usually doesn't need to access Rhai internal data structures. | diff --git a/doc/src/start/examples.md b/doc/src/start/examples/index.md similarity index 88% rename from doc/src/start/examples.md rename to doc/src/start/examples/index.md index b7f9ae1d..6af1495b 100644 --- a/doc/src/start/examples.md +++ b/doc/src/start/examples/index.md @@ -1,7 +1,7 @@ Examples ======== -{{#include ../links.md}} +{{#include ../../links.md}} Rhai comes with a number of examples showing how to integrate the scripting [`Engine`] within a Rust application, as well as a number of sample scripts that showcase different Rhai language features. diff --git a/doc/src/start/examples/rust.md b/doc/src/start/examples/rust.md index d7a28271..1803c49b 100644 --- a/doc/src/start/examples/rust.md +++ b/doc/src/start/examples/rust.md @@ -5,17 +5,17 @@ Rust Examples A number of examples can be found in the `examples` folder: -| Example | Description | -| ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| [`arrays_and_structs`](https://github.com/jonathandturner/rhai/tree/master/examples/arrays_and_structs.rs) | shows how to register a custom Rust type and using [arrays] on it | -| [`custom_types_and_methods`](https://github.com/jonathandturner/rhai/tree/master/examples/custom_types_and_methods.rs) | shows how to register a custom Rust type and methods for it | -| [`hello`](https://github.com/jonathandturner/rhai/tree/master/examples/hello.rs) | simple example that evaluates an expression and prints the result | -| [`no_std`](https://github.com/jonathandturner/rhai/tree/master/examples/no_std.rs) | example to test out `no-std` builds | -| [`reuse_scope`](https://github.com/jonathandturner/rhai/tree/master/examples/reuse_scope.rs) | evaluates two pieces of code in separate runs, but using a common [`Scope`] | -| [`rhai_runner`](https://github.com/jonathandturner/rhai/tree/master/examples/rhai_runner.rs) | runs each filename passed to it as a Rhai script | -| [`simple_fn`](https://github.com/jonathandturner/rhai/tree/master/examples/simple_fn.rs) | shows how to register a simple function | -| [`strings`](https://github.com/jonathandturner/rhai/tree/master/examples/strings.rs) | shows different ways to register functions taking string arguments | -| [`repl`](https://github.com/jonathandturner/rhai/tree/master/examples/repl.rs) | a simple REPL, interactively evaluate statements from stdin | +| Example | Description | +| ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| [`arrays_and_structs`](https://github.com/jonathandturner/rhai/tree/master/examples/arrays_and_structs.rs) | Shows how to register a custom Rust type and using [arrays] on it. | +| [`custom_types_and_methods`](https://github.com/jonathandturner/rhai/tree/master/examples/custom_types_and_methods.rs) | Shows how to register a custom Rust type and methods for it. | +| [`hello`](https://github.com/jonathandturner/rhai/tree/master/examples/hello.rs) | Simple example that evaluates an expression and prints the result. | +| [`no_std`](https://github.com/jonathandturner/rhai/tree/master/examples/no_std.rs) | Example to test out `no-std` builds. | +| [`reuse_scope`](https://github.com/jonathandturner/rhai/tree/master/examples/reuse_scope.rs) | Evaluates two pieces of code in separate runs, but using a common [`Scope`]. | +| [`rhai_runner`](https://github.com/jonathandturner/rhai/tree/master/examples/rhai_runner.rs) | Runs each filename passed to it as a Rhai script. | +| [`simple_fn`](https://github.com/jonathandturner/rhai/tree/master/examples/simple_fn.rs) | Shows how to register a simple function. | +| [`strings`](https://github.com/jonathandturner/rhai/tree/master/examples/strings.rs) | Shows different ways to register functions taking string arguments. | +| [`repl`](https://github.com/jonathandturner/rhai/tree/master/examples/repl.rs) | A simple REPL, interactively evaluate statements from stdin. | The `repl` example is a particularly good one as it allows one to interactively try out Rhai's language features in a standard REPL (**R**ead-**E**val-**P**rint **L**oop). diff --git a/doc/src/start/examples/scripts.md b/doc/src/start/examples/scripts.md index 50cfba4e..20836aef 100644 --- a/doc/src/start/examples/scripts.md +++ b/doc/src/start/examples/scripts.md @@ -10,21 +10,22 @@ There are also a number of examples scripts that showcase Rhai's features, all i | Script | Description | | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -| [`array.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/array.rhai) | [arrays] in Rhai | -| [`assignment.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/assignment.rhai) | variable declarations | -| [`comments.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/comments.rhai) | just comments | +| [`array.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/array.rhai) | [Arrays] | +| [`assignment.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/assignment.rhai) | Variable declarations | +| [`comments.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/comments.rhai) | Just comments | | [`for1.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/for1.rhai) | [`for`](#for-loop) loops | | [`for2.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/for2.rhai) | [`for`](#for-loop) loops on [arrays] | -| [`function_decl1.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/function_decl1.rhai) | a [function] without parameters | -| [`function_decl2.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/function_decl2.rhai) | a [function] with two parameters | -| [`function_decl3.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/function_decl3.rhai) | a [function] with many parameters | +| [`function_decl1.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/function_decl1.rhai) | A [function] without parameters | +| [`function_decl2.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/function_decl2.rhai) | A [function] with two parameters | +| [`function_decl3.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/function_decl3.rhai) | A [function] with many parameters | | [`if1.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/if1.rhai) | [`if`](#if-statement) example | -| [`loop.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/loop.rhai) | count-down [`loop`](#infinite-loop) in Rhai, emulating a `do` .. `while` loop | -| [`op1.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/op1.rhai) | just simple addition | -| [`op2.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/op2.rhai) | simple addition and multiplication | -| [`op3.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/op3.rhai) | change evaluation order with parenthesis | -| [`string.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/string.rhai) | [string] operations | -| [`strings_map.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/strings_map.rhai) | [string] and [object map] operations | +| [`loop.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/loop.rhai) | Count-down [`loop`](#infinite-loop) in Rhai, emulating a `do` .. `while` loop | +| [`oop.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/oop.rhai) | Simulate [object-oriented programming (OOP)][OOP] | +| [`op1.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/op1.rhai) | Just simple addition | +| [`op2.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/op2.rhai) | Simple addition and multiplication | +| [`op3.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/op3.rhai) | Change evaluation order with parenthesis | +| [`string.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/string.rhai) | [String] operations | +| [`strings_map.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/strings_map.rhai) | [String] and [object map] operations | | [`while.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/while.rhai) | [`while`](#while-loop) loop | @@ -33,12 +34,12 @@ Benchmark Scripts The following scripts are for benchmarking the speed of Rhai: -| Scripts | Description | -| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | -| [`speed_test.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/speed_test.rhai) | a simple program to measure the speed of Rhai's interpreter (1 million iterations) | -| [`primes.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/primes.rhai) | use Sieve of Eratosthenes to find all primes smaller than a limit | -| [`fibonacci.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/fibonacci.rhai) | calculate the n-th Fibonacci number using a really dumb algorithm | -| [`mat_mul.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/mat_mul.rhai) | matrix multiplication test to measure the speed of multi-dimensional array access | +| Scripts | Description | +| ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | +| [`speed_test.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/speed_test.rhai) | A simple program to measure the speed of Rhai's interpreter (1 million iterations). | +| [`primes.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/primes.rhai) | Use Sieve of Eratosthenes to find all primes smaller than a limit. | +| [`fibonacci.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/fibonacci.rhai) | Calculate the n-th Fibonacci number using a really dumb algorithm. | +| [`mat_mul.rhai`](https://github.com/jonathandturner/rhai/tree/master/scripts/mat_mul.rhai) | Matrix multiplication test to measure the speed of multi-dimensional array access. | Running Example Scripts diff --git a/doc/src/start/features.md b/doc/src/start/features.md index fca919a5..b4a415a9 100644 --- a/doc/src/start/features.md +++ b/doc/src/start/features.md @@ -4,24 +4,27 @@ Optional Features {{#include ../links.md}} By default, Rhai includes all the standard functionalities in a small, tight package. + Most features are here to opt-**out** of certain functionalities that are not needed. +Notice that this deviates from Rust norm where features are _additive_. -Excluding unneeded functionalities can result in smaller, faster builds -as well as more control over what a script can (or cannot) do. +Excluding unneeded functionalities can result in smaller, faster builds as well as +more control over what a script can (or cannot) do. -| Feature | Description | -| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `unchecked` | Disable arithmetic checking (such as over-flows and division by zero), call stack depth limit, operations count limit and modules loading limit. Beware that a bad script may panic the entire system! | -| `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, all Rhai types, including [`Engine`], [`Scope`] and `AST`, are all `Send + Sync`. | -| `no_optimize` | Disable the script optimizer. | -| `no_float` | Disable floating-point numbers and math. | -| `only_i32` | Set the system integer type to `i32` and disable all other integer types. `INT` is set to `i32`. | -| `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. | -| `no_index` | Disable [arrays] and indexing features. | -| `no_object` | Disable support for custom types and [object maps]. | -| `no_function` | Disable script-defined functions. | -| `no_module` | Disable loading external modules. | -| `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | +| Feature | Description | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `unchecked` | Disable arithmetic checking (such as over-flows and division by zero), call stack depth limit, operations count limit and modules loading limit.
Beware that a bad script may panic the entire system! | +| `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, all Rhai types, including [`Engine`], [`Scope`] and [`AST`], are all `Send + Sync`. | +| `no_optimize` | Disable [script optimization]. | +| `no_float` | Disable floating-point numbers and math. | +| `only_i32` | Set the system integer type to `i32` and disable all other integer types. `INT` is set to `i32`. | +| `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. | +| `no_index` | Disable [arrays] and indexing features. | +| `no_object` | Disable support for [custom types] and [object maps]. | +| `no_function` | Disable script-defined [functions]. | +| `no_module` | Disable loading external [modules]. | +| `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | +| `internals` | Expose internal data structures (e.g. [`AST`] nodes). Beware that Rhai internals are volatile and may change from version to version. | Example @@ -30,19 +33,19 @@ Example The `Cargo.toml` configuration below turns on these six features: * `sync` (everything `Send + Sync`) -* `unchecked` (no checked arithmetic - should not be used with untrusted user scripts) +* `unchecked` (disable all checking - should not be used with untrusted user scripts) * `only_i32` (only 32-bit signed integers) * `no_float` (no floating point numbers) -* `no_module` (no loading external modules) -* `no_function` (no defining functions) +* `no_module` (no loading external [modules]) +* `no_function` (no defining [functions]) ```toml [dependencies] -rhai = { version = "0.15.2", features = [ "sync", "unchecked", "only_i32", "no_float", "no_module", "no_function" ] } +rhai = { version = "{{version}}", features = [ "sync", "unchecked", "only_i32", "no_float", "no_module", "no_function" ] } ``` -The resulting scripting engine supports only the `i32` integer numeral type (and no others like `u32` or `i16`), -no floating-point, is `Send + Sync` (so it can be safely used across threads), does not support defining functions -nor loading external modules. +The resulting scripting engine supports only the `i32` integer numeral type (and no others like `u32`, `i16` or `i64`), +no floating-point, is `Send + Sync` (so it can be safely used across threads), does not support defining [functions] +nor loading external [modules]. This configuration is perfect for an expression parser in a 32-bit embedded system without floating-point hardware. diff --git a/doc/src/start.md b/doc/src/start/index.md similarity index 81% rename from doc/src/start.md rename to doc/src/start/index.md index 33de6526..f89dfbd0 100644 --- a/doc/src/start.md +++ b/doc/src/start/index.md @@ -1,6 +1,6 @@ Getting Started =============== -{{#include links.md}} +{{#include ../links.md}} This section shows how to install the Rhai crate into a Rust application. diff --git a/doc/src/start/install.md b/doc/src/start/install.md index c3553298..a7dc944f 100644 --- a/doc/src/start/install.md +++ b/doc/src/start/install.md @@ -3,15 +3,17 @@ Install the Rhai Crate {{#include ../links.md}} -Install the Rhai crate from [`crates.io`](https:/crates.io/crates/rhai/) by adding this line -under `dependencies` in `Cargo.toml`: +In order to use Rhai in a project, the Rhai crate must first be made a dependency. + +The easiest way is to install the Rhai crate from [`crates.io`](https:/crates.io/crates/rhai/), +starting by looking up the latest version and adding this line under `dependencies` in the project's `Cargo.toml`: ```toml [dependencies] -rhai = "0.15.2" +rhai = "{{version}}" # assuming {{version}} is the latest version ``` -Use the latest released crate version on [`crates.io`](https:/crates.io/crates/rhai/): +Or to automatically use the latest released crate version on [`crates.io`](https:/crates.io/crates/rhai/): ```toml [dependencies] diff --git a/doc/src/start/playground.md b/doc/src/start/playground.md new file mode 100644 index 00000000..08809805 --- /dev/null +++ b/doc/src/start/playground.md @@ -0,0 +1,10 @@ +Online Playground +================= + +{{#include ../links.md}} + +Rhai provides an [online playground][playground] to try out its language and engine features +without having to install anything. + +The playground provides a syntax-highlighting script editor with example snippets. +Scripts can be evaluated directly from the editor. diff --git a/examples/repl.rs b/examples/repl.rs index f18a56e5..cf255986 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -48,6 +48,7 @@ fn print_error(input: &str, err: EvalAltResult) { fn print_help() { println!("help => print this help"); println!("quit, exit => quit"); + println!("scope => print all variables in the scope"); println!("ast => print the last AST"); println!("astu => print the last raw, un-optimized AST"); println!(r"end a line with '\' to continue to the next line."); @@ -110,6 +111,13 @@ fn main() { continue; } "exit" | "quit" => break, // quit + "scope" => { + scope + .iter() + .enumerate() + .for_each(|(i, (name, value))| println!("[{}] {} = {:?}", i + 1, name, value)); + continue; + } "astu" => { // print the last un-optimized AST println!("{:#?}", &ast_u); @@ -158,7 +166,6 @@ fn main() { } // Throw away all the statements, leaving only the functions - #[cfg(not(feature = "no_function"))] - main_ast.retain_functions(); + main_ast.clear_statements(); } } diff --git a/rhai_logo.png b/rhai_logo.png new file mode 100644 index 00000000..af45aa7f Binary files /dev/null and b/rhai_logo.png differ diff --git a/scripts/for1.rhai b/scripts/for1.rhai index 8fc6c95e..345d17a7 100644 --- a/scripts/for1.rhai +++ b/scripts/for1.rhai @@ -12,6 +12,6 @@ for a in arr { //print(a); // <- if you uncomment this line, the script will fail to run // because 'a' is not defined here -for i in range(0, 5) { // runs through a range from 1 to 5 exclusive +for i in range(0, 5) { // runs through a range from 0 to 4 print(i); } diff --git a/scripts/oop.rhai b/scripts/oop.rhai new file mode 100644 index 00000000..fe03b636 --- /dev/null +++ b/scripts/oop.rhai @@ -0,0 +1,45 @@ +// This script simulates object-oriented programming (OOP) techniques +// using function pointers (Fn) and object maps. + +// Define object +let obj1 = #{ + _data: 42, // data field + get_data: Fn("getData"), // property getter + action: Fn("action"), // method + update: Fn("update1") // property setter +}; + +fn getData() { + this._data +} +fn action() { + print("Data=" + this._data); +} +fn update1(x) { + this._data = x; + this.action(); +} + +if obj1.get_data() > 0 { // property access + obj1.update(123); // call method +} else { + print("we have a problem here"); +} + +// Define another object based on the first object +let obj2 = #{ + _data: 0, // data field - new value + update: Fn("update2") // property setter - another function +}; +obj2.fill_with(obj1); // add all other fields from obj1 + +fn update2(x) { + this._data = x * 2; + this.action(); +} + +if obj2.get_data() > 0 { // property access + obj2.update(0); // call method +} else { + obj2.update(42); // call method +} diff --git a/scripts/string.rhai b/scripts/string.rhai index 835b2556..38dc52e9 100644 --- a/scripts/string.rhai +++ b/scripts/string.rhai @@ -10,8 +10,8 @@ print("foo" < "bar"); // string comparison print("foo" >= "bar"); // string comparison print("the answer is " + 42); // string building using non-string types -let s = "hello, world!"; // string variable -print("length=" + s.len); // should be 13 +let s = "\u2764" hello, world! \U0001F603"; // string variable +print("length=" + s.len); // should be 17 -s[s.len-1] = '?'; // change the string +s[s.len-3] = '?'; // change the string print("Question: " + s); // should print 'Question: hello, world?' diff --git a/src/any.rs b/src/any.rs index 6a5d9050..7e8b8072 100644 --- a/src/any.rs +++ b/src/any.rs @@ -1,7 +1,6 @@ //! Helper module which defines the `Any` trait to to allow dynamic value handling. -use crate::fn_native::SendSync; -use crate::module::Module; +use crate::fn_native::{FnPtr, SendSync}; use crate::parser::{ImmutableString, INT}; use crate::r#unsafe::{unsafe_cast_box, unsafe_try_cast}; @@ -91,13 +90,13 @@ pub trait Variant: Any + Send + Sync { impl Variant for T { fn as_any(&self) -> &dyn Any { - self as &dyn Any + self } fn as_mut_any(&mut self) -> &mut dyn Any { - self as &mut dyn Any + self } fn as_box_any(self: Box) -> Box { - self as Box + self } fn type_name(&self) -> &'static str { type_name::() @@ -138,7 +137,7 @@ pub enum Union { Array(Box), #[cfg(not(feature = "no_object"))] Map(Box), - Module(Box), + FnPtr(FnPtr), Variant(Box>), } @@ -175,7 +174,7 @@ impl Dynamic { Union::Array(_) => TypeId::of::(), #[cfg(not(feature = "no_object"))] Union::Map(_) => TypeId::of::(), - Union::Module(_) => TypeId::of::(), + Union::FnPtr(_) => TypeId::of::(), Union::Variant(value) => (***value).type_id(), } } @@ -194,7 +193,7 @@ impl Dynamic { Union::Array(_) => "array", #[cfg(not(feature = "no_object"))] Union::Map(_) => "map", - Union::Module(_) => "sub-scope", + Union::FnPtr(_) => "Fn", #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] @@ -218,7 +217,7 @@ impl fmt::Display for Dynamic { Union::Array(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_object"))] Union::Map(value) => write!(f, "#{:?}", value), - Union::Module(value) => fmt::Debug::fmt(value, f), + Union::FnPtr(value) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] @@ -242,7 +241,7 @@ impl fmt::Debug for Dynamic { Union::Array(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_object"))] Union::Map(value) => write!(f, "#{:?}", value), - Union::Module(value) => fmt::Debug::fmt(value, f), + Union::FnPtr(value) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] @@ -266,7 +265,7 @@ 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())), - Union::Module(ref value) => Self(Union::Module(value.clone())), + Union::FnPtr(ref value) => Self(Union::FnPtr(value.clone())), Union::Variant(ref value) => (***value).clone_into_dynamic(), } } @@ -311,58 +310,52 @@ impl Dynamic { /// assert_eq!(new_result.to_string(), "hello"); /// ``` pub fn from(value: T) -> Self { - let dyn_value = &value as &dyn Any; - - if let Some(result) = dyn_value.downcast_ref::<()>().cloned().map(Union::Unit) { - return Self(result); - } else if let Some(result) = dyn_value.downcast_ref::().cloned().map(Union::Bool) { - return Self(result); - } else if let Some(result) = dyn_value.downcast_ref::().cloned().map(Union::Int) { - return Self(result); - } else if let Some(result) = dyn_value.downcast_ref::().cloned().map(Union::Char) { - return Self(result); - } else if let Some(result) = dyn_value - .downcast_ref::() - .cloned() - .map(Union::Str) - { - return Self(result); + if let Some(result) = ::downcast_ref::<()>(&value) { + return result.clone().into(); + } else if let Some(result) = ::downcast_ref::(&value) { + return result.clone().into(); + } else if let Some(result) = ::downcast_ref::(&value) { + return result.clone().into(); + } else if let Some(result) = ::downcast_ref::(&value) { + return result.clone().into(); + } else if let Some(result) = ::downcast_ref::(&value) { + return result.clone().into(); } #[cfg(not(feature = "no_float"))] { - if let Some(result) = dyn_value.downcast_ref::().cloned().map(Union::Float) { - return Self(result); + if let Some(result) = ::downcast_ref::(&value) { + return result.clone().into(); } } - let mut var = Box::new(value); + let mut boxed = Box::new(value); - var = match unsafe_cast_box::<_, Dynamic>(var) { + boxed = match unsafe_cast_box::<_, Dynamic>(boxed) { Ok(d) => return *d, - Err(var) => var, + Err(val) => val, }; - var = match unsafe_cast_box::<_, String>(var) { - Ok(s) => return Self(Union::Str(s.into())), - Err(var) => var, + boxed = match unsafe_cast_box::<_, String>(boxed) { + Ok(s) => return (*s).into(), + Err(val) => val, }; #[cfg(not(feature = "no_index"))] { - var = match unsafe_cast_box::<_, Array>(var) { - Ok(array) => return Self(Union::Array(array)), - Err(var) => var, + boxed = match unsafe_cast_box::<_, Array>(boxed) { + Ok(array) => return (*array).into(), + Err(val) => val, }; } #[cfg(not(feature = "no_object"))] { - var = match unsafe_cast_box::<_, Map>(var) { - Ok(map) => return Self(Union::Map(map)), - Err(var) => var, + boxed = match unsafe_cast_box::<_, Map>(boxed) { + Ok(map) => return (*map).into(), + Err(val) => val, } } - Self(Union::Variant(Box::new(var))) + Self(Union::Variant(Box::new(boxed))) } /// Get a copy of the `Dynamic` value as a specific type. @@ -401,7 +394,7 @@ 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), - Union::Module(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), + Union::FnPtr(value) => unsafe_try_cast(value), Union::Variant(value) => (*value).as_box_any().downcast().map(|x| *x).ok(), } } @@ -444,7 +437,7 @@ 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(), - Union::Module(value) => *unsafe_cast_box::<_, T>(value).unwrap(), + Union::FnPtr(value) => unsafe_try_cast(value).unwrap(), Union::Variant(value) => (*value).as_box_any().downcast().map(|x| *x).unwrap(), } } @@ -454,24 +447,23 @@ impl Dynamic { /// Returns `None` if the cast fails. pub fn downcast_ref(&self) -> Option<&T> { if TypeId::of::() == TypeId::of::() { - return (self as &dyn Any).downcast_ref::(); + return ::downcast_ref::(self); } match &self.0 { - Union::Unit(value) => (value as &dyn Any).downcast_ref::(), - Union::Bool(value) => (value as &dyn Any).downcast_ref::(), - Union::Str(value) => (value as &dyn Any) - .downcast_ref::() - .or_else(|| (value.as_ref() as &dyn Any).downcast_ref::()), - Union::Char(value) => (value as &dyn Any).downcast_ref::(), - Union::Int(value) => (value as &dyn Any).downcast_ref::(), + Union::Unit(value) => ::downcast_ref::(value), + Union::Bool(value) => ::downcast_ref::(value), + Union::Str(value) => ::downcast_ref::(value) + .or_else(|| ::downcast_ref::(value.as_ref())), + Union::Char(value) => ::downcast_ref::(value), + Union::Int(value) => ::downcast_ref::(value), #[cfg(not(feature = "no_float"))] - Union::Float(value) => (value as &dyn Any).downcast_ref::(), + Union::Float(value) => ::downcast_ref::(value), #[cfg(not(feature = "no_index"))] - Union::Array(value) => (value.as_ref() as &dyn Any).downcast_ref::(), + Union::Array(value) => ::downcast_ref::(value.as_ref()), #[cfg(not(feature = "no_object"))] - Union::Map(value) => (value.as_ref() as &dyn Any).downcast_ref::(), - Union::Module(value) => (value.as_ref() as &dyn Any).downcast_ref::(), + Union::Map(value) => ::downcast_ref::(value.as_ref()), + Union::FnPtr(value) => ::downcast_ref::(value), Union::Variant(value) => value.as_ref().as_ref().as_any().downcast_ref::(), } } @@ -481,22 +473,22 @@ impl Dynamic { /// Returns `None` if the cast fails. pub fn downcast_mut(&mut self) -> Option<&mut T> { if TypeId::of::() == TypeId::of::() { - return (self as &mut dyn Any).downcast_mut::(); + return ::downcast_mut::(self); } match &mut self.0 { - Union::Unit(value) => (value as &mut dyn Any).downcast_mut::(), - Union::Bool(value) => (value as &mut dyn Any).downcast_mut::(), - Union::Str(value) => (value as &mut dyn Any).downcast_mut::(), - Union::Char(value) => (value as &mut dyn Any).downcast_mut::(), - Union::Int(value) => (value as &mut dyn Any).downcast_mut::(), + Union::Unit(value) => ::downcast_mut::(value), + Union::Bool(value) => ::downcast_mut::(value), + Union::Str(value) => ::downcast_mut::(value), + Union::Char(value) => ::downcast_mut::(value), + Union::Int(value) => ::downcast_mut::(value), #[cfg(not(feature = "no_float"))] - Union::Float(value) => (value as &mut dyn Any).downcast_mut::(), + Union::Float(value) => ::downcast_mut::(value), #[cfg(not(feature = "no_index"))] - Union::Array(value) => (value.as_mut() as &mut dyn Any).downcast_mut::(), + Union::Array(value) => ::downcast_mut::(value.as_mut()), #[cfg(not(feature = "no_object"))] - Union::Map(value) => (value.as_mut() as &mut dyn Any).downcast_mut::(), - Union::Module(value) => (value.as_mut() as &mut dyn Any).downcast_mut::(), + Union::Map(value) => ::downcast_mut::(value.as_mut()), + Union::FnPtr(value) => ::downcast_mut::(value), Union::Variant(value) => value.as_mut().as_mut_any().downcast_mut::(), } } @@ -543,6 +535,7 @@ impl Dynamic { pub fn as_str(&self) -> Result<&str, &'static str> { match &self.0 { Union::Str(s) => Ok(s), + Union::FnPtr(f) => Ok(f.fn_name()), _ => Err(self.type_name()), } } @@ -550,8 +543,16 @@ impl Dynamic { /// Convert the `Dynamic` into `String` and return it. /// Returns the name of the actual type if the cast fails. pub fn take_string(self) -> Result { + self.take_immutable_string() + .map(ImmutableString::into_owned) + } + + /// Convert the `Dynamic` into `ImmutableString` and return it. + /// Returns the name of the actual type if the cast fails. + pub(crate) fn take_immutable_string(self) -> Result { match self.0 { - Union::Str(s) => Ok(s.into_owned()), + Union::Str(s) => Ok(s), + Union::FnPtr(f) => Ok(f.take_fn_name()), _ => Err(self.type_name()), } } @@ -583,16 +584,11 @@ impl From for Dynamic { Self(Union::Char(value)) } } -impl From for Dynamic { - fn from(value: String) -> Self { +impl> From for Dynamic { + fn from(value: S) -> Self { Self(Union::Str(value.into())) } } -impl From for Dynamic { - fn from(value: ImmutableString) -> Self { - Self(Union::Str(value)) - } -} #[cfg(not(feature = "no_index"))] impl From> for Dynamic { fn from(value: Vec) -> Self { @@ -610,16 +606,21 @@ impl From<&[T]> for Dynamic { } } #[cfg(not(feature = "no_object"))] -impl From> for Dynamic { - fn from(value: HashMap) -> Self { +impl, T: Variant + Clone> From> for Dynamic { + fn from(value: HashMap) -> Self { Self(Union::Map(Box::new( value .into_iter() - .map(|(k, v)| (k, Dynamic::from(v))) + .map(|(k, v)| (k.into(), Dynamic::from(v))) .collect(), ))) } } +impl From for Dynamic { + fn from(value: FnPtr) -> Self { + Self(Union::FnPtr(value)) + } +} /// Private type which ensures that `rhai::Any` and `rhai::AnyExt` can only /// be implemented by this crate. diff --git a/src/api.rs b/src/api.rs index ebfb699c..2f0e3e19 100644 --- a/src/api.rs +++ b/src/api.rs @@ -2,8 +2,8 @@ use crate::any::{Dynamic, Variant}; use crate::engine::{ - get_script_function_by_signature, make_getter, make_setter, Engine, State, FUNC_INDEXER_GET, - FUNC_INDEXER_SET, + get_script_function_by_signature, make_getter, make_setter, Engine, Imports, State, FN_IDX_GET, + FN_IDX_SET, }; use crate::error::ParseError; use crate::fn_call::FuncArgs; @@ -323,7 +323,7 @@ impl Engine { U: Variant + Clone, X: Variant + Clone, { - self.register_fn(FUNC_INDEXER_GET, callback); + self.register_fn(FN_IDX_GET, callback); } /// Register an index setter for a registered type with the `Engine`. @@ -371,7 +371,7 @@ impl Engine { U: Variant + Clone, X: Variant + Clone, { - self.register_fn(FUNC_INDEXER_SET, callback); + self.register_fn(FN_IDX_SET, callback); } /// Shorthand for register both index getter and setter functions for a registered type with the `Engine`. @@ -422,7 +422,7 @@ impl Engine { self.register_indexer_set(setter); } - /// Compile a string into an `AST`, which can be used later for evaluation. + /// Compile a string into an [`AST`], which can be used later for evaluation. /// /// # Example /// @@ -445,7 +445,7 @@ impl Engine { self.compile_with_scope(&Scope::new(), script) } - /// Compile a string into an `AST` using own scope, which can be used later for evaluation. + /// Compile a string into an [`AST`] using own scope, which can be used later for evaluation. /// /// The scope is useful for passing constants into the script for optimization /// when using `OptimizationLevel::Full`. @@ -488,7 +488,7 @@ impl Engine { } /// When passed a list of strings, first join the strings into one large script, - /// and then compile them into an `AST` using own scope, which can be used later for evaluation. + /// and then compile them into an [`AST`] using own scope, which can be used later for evaluation. /// /// The scope is useful for passing constants into the script for optimization /// when using `OptimizationLevel::Full`. @@ -541,7 +541,7 @@ impl Engine { self.compile_with_scope_and_optimization_level(scope, scripts, self.optimization_level) } - /// Join a list of strings and compile into an `AST` using own scope at a specific optimization level. + /// Join a list of strings and compile into an [`AST`] using own scope at a specific optimization level. pub(crate) fn compile_with_scope_and_optimization_level( &self, scope: &Scope, @@ -577,7 +577,7 @@ impl Engine { Ok(contents) } - /// Compile a script file into an `AST`, which can be used later for evaluation. + /// Compile a script file into an [`AST`], which can be used later for evaluation. /// /// # Example /// @@ -603,7 +603,7 @@ impl Engine { self.compile_file_with_scope(&Scope::new(), path) } - /// Compile a script file into an `AST` using own scope, which can be used later for evaluation. + /// Compile a script file into an [`AST`] using own scope, which can be used later for evaluation. /// /// The scope is useful for passing constants into the script for optimization /// when using `OptimizationLevel::Full`. @@ -685,7 +685,7 @@ impl Engine { self.eval_ast_with_scope(&mut scope, &ast) } - /// Compile a string containing an expression into an `AST`, + /// Compile a string containing an expression into an [`AST`], /// which can be used later for evaluation. /// /// # Example @@ -709,7 +709,7 @@ impl Engine { self.compile_expression_with_scope(&Scope::new(), script) } - /// Compile a string containing an expression into an `AST` using own scope, + /// Compile a string containing an expression into an [`AST`] using own scope, /// which can be used later for evaluation. /// /// The scope is useful for passing constants into the script for optimization @@ -917,7 +917,7 @@ impl Engine { self.eval_ast_with_scope(scope, &ast) } - /// Evaluate an `AST`. + /// Evaluate an [`AST`]. /// /// # Example /// @@ -939,7 +939,7 @@ impl Engine { self.eval_ast_with_scope(&mut Scope::new(), ast) } - /// Evaluate an `AST` with own scope. + /// Evaluate an [`AST`] with own scope. /// /// # Example /// @@ -973,7 +973,8 @@ impl Engine { scope: &mut Scope, ast: &AST, ) -> Result> { - let (result, _) = self.eval_ast_with_scope_raw(scope, ast)?; + let mut mods = Imports::new(); + let (result, _) = self.eval_ast_with_scope_raw(scope, &mut mods, ast)?; let return_type = self.map_type_name(result.type_name()); @@ -985,18 +986,19 @@ impl Engine { }); } - /// Evaluate an `AST` with own scope. - pub(crate) fn eval_ast_with_scope_raw( + /// Evaluate an [`AST`] with own scope. + pub(crate) fn eval_ast_with_scope_raw<'a>( &self, scope: &mut Scope, - ast: &AST, + mods: &mut Imports, + ast: &'a AST, ) -> Result<(Dynamic, u64), Box> { let mut state = State::new(); ast.statements() .iter() .try_fold(().into(), |_, stmt| { - self.eval_stmt(scope, &mut state, ast.lib(), stmt, 0) + self.eval_stmt(scope, mods, &mut state, ast.lib(), &mut None, stmt, 0) }) .or_else(|err| match *err { EvalAltResult::Return(out, _) => Ok(out), @@ -1050,7 +1052,7 @@ impl Engine { self.consume_ast_with_scope(&mut Scope::new(), ast) } - /// Evaluate an `AST` with own scope, but throw away the result and only return error (if any). + /// Evaluate an [`AST`] 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. pub fn consume_ast_with_scope( &self, @@ -1058,11 +1060,12 @@ impl Engine { ast: &AST, ) -> Result<(), Box> { let mut state = State::new(); + let mut mods = Default::default(); ast.statements() .iter() .try_fold(().into(), |_, stmt| { - self.eval_stmt(scope, &mut state, ast.lib(), stmt, 0) + self.eval_stmt(scope, &mut mods, &mut state, ast.lib(), &mut None, stmt, 0) }) .map_or_else( |err| match *err { @@ -1073,7 +1076,7 @@ impl Engine { ) } - /// Call a script function defined in an `AST` with multiple arguments. + /// Call a script function defined in an [`AST`] with multiple arguments. /// Arguments are passed as a tuple. /// /// # Example @@ -1130,7 +1133,7 @@ impl Engine { }); } - /// Call a script function defined in an `AST` with multiple `Dynamic` arguments. + /// Call a script function defined in an [`AST`] with multiple `Dynamic` arguments. /// /// # Example /// @@ -1176,7 +1179,7 @@ impl Engine { self.call_fn_dynamic_raw(scope, ast, name, arg_values.as_mut()) } - /// Call a script function defined in an `AST` with multiple `Dynamic` arguments. + /// Call a script function defined in an [`AST`] with multiple `Dynamic` arguments. /// /// ## WARNING /// @@ -1201,20 +1204,31 @@ impl Engine { })?; let mut state = State::new(); + let mut mods = Imports::new(); let args = args.as_mut(); - self.call_script_fn(scope, &mut state, ast.lib(), name, fn_def, args, 0) + self.call_script_fn( + scope, + &mut mods, + &mut state, + ast.lib(), + &mut None, + name, + fn_def, + args, + 0, + ) } - /// Optimize the `AST` with constants defined in an external Scope. - /// An optimized copy of the `AST` is returned while the original `AST` is consumed. + /// Optimize the [`AST`] with constants defined in an external Scope. + /// An optimized copy of the [`AST`] is returned while the original [`AST`] is consumed. /// /// Although optimization is performed by default during compilation, sometimes it is necessary to /// _re_-optimize an AST. For example, when working with constants that are passed in via an - /// external scope, it will be more efficient to optimize the `AST` once again to take advantage + /// external scope, it will be more efficient to optimize the [`AST`] once again to take advantage /// of the new constants. /// - /// With this method, it is no longer necessary to recompile a large script. The script `AST` can be + /// With this method, it is no longer necessary to recompile a large script. The script [`AST`] can be /// compiled just once. Before evaluation, constants are passed into the `Engine` via an external scope /// (i.e. with `scope.push_constant(...)`). Then, the `AST is cloned and the copy re-optimized before running. #[cfg(not(feature = "no_optimize"))] diff --git a/src/engine.rs b/src/engine.rs index f1260824..65c91735 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -3,15 +3,15 @@ use crate::any::{Dynamic, Union, Variant}; use crate::calc_fn_hash; use crate::error::ParseErrorType; -use crate::fn_native::{CallableFunction, Callback, FnCallArgs}; -use crate::module::{resolvers, Module, ModuleResolver}; +use crate::fn_native::{CallableFunction, Callback, FnCallArgs, FnPtr}; +use crate::module::{resolvers, Module, ModuleRef, ModuleResolver}; use crate::optimize::OptimizationLevel; 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::r#unsafe::unsafe_cast_var_name_to_lifetime; use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; -use crate::token::Position; +use crate::token::{is_valid_identifier, Position}; use crate::utils::StaticVec; #[cfg(not(feature = "no_float"))] @@ -19,6 +19,7 @@ use crate::parser::FLOAT; use crate::stdlib::{ any::TypeId, + borrow::Cow, boxed::Box, collections::HashMap, format, @@ -38,7 +39,10 @@ pub type Array = Vec; /// /// Not available under the `no_object` feature. #[cfg(not(feature = "no_object"))] -pub type Map = HashMap; +pub type Map = HashMap; + +/// A stack of imported modules. +pub type Imports<'a> = Vec<(Cow<'a, str>, Module)>; #[cfg(not(feature = "unchecked"))] #[cfg(debug_assertions)] @@ -71,11 +75,22 @@ pub const KEYWORD_PRINT: &str = "print"; pub const KEYWORD_DEBUG: &str = "debug"; pub const KEYWORD_TYPE_OF: &str = "type_of"; 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_GET: &str = "$index$get$"; -pub const FUNC_INDEXER_SET: &str = "$index$set$"; +pub const KEYWORD_FN_PTR: &str = "Fn"; +pub const KEYWORD_FN_PTR_CALL: &str = "call"; +pub const KEYWORD_THIS: &str = "this"; +pub const FN_TO_STRING: &str = "to_string"; +pub const FN_GET: &str = "get$"; +pub const FN_SET: &str = "set$"; +pub const FN_IDX_GET: &str = "$index$get$"; +pub const FN_IDX_SET: &str = "$index$set$"; + +/// A type specifying the method of chaining. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +enum ChainType { + None, + Index, + Dot, +} /// A type that encapsulates a mutation target for an expression with side effects. #[derive(Debug)] @@ -178,7 +193,7 @@ impl> From for Target<'_> { /// /// This type uses some unsafe code, mainly for avoiding cloning of local variable names via /// direct lifetime casting. -#[derive(Debug, Eq, PartialEq, Hash, Clone, Default)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, 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. @@ -297,7 +312,7 @@ impl Default for Engine { #[cfg(any(feature = "no_module", feature = "no_std", target_arch = "wasm32",))] module_resolver: None, - type_names: HashMap::new(), + type_names: Default::default(), // default print/debug implementations print: Box::new(default_print), @@ -331,15 +346,15 @@ impl Default for Engine { /// Make getter function pub fn make_getter(id: &str) -> String { - format!("{}{}", FUNC_GETTER, id) + format!("{}{}", FN_GET, id) } /// Extract the property name from a getter function name. fn extract_prop_from_getter(fn_name: &str) -> Option<&str> { #[cfg(not(feature = "no_object"))] { - if fn_name.starts_with(FUNC_GETTER) { - Some(&fn_name[FUNC_GETTER.len()..]) + if fn_name.starts_with(FN_GET) { + Some(&fn_name[FN_GET.len()..]) } else { None } @@ -352,15 +367,15 @@ fn extract_prop_from_getter(fn_name: &str) -> Option<&str> { /// Make setter function pub fn make_setter(id: &str) -> String { - format!("{}{}", FUNC_SETTER, id) + format!("{}{}", FN_SET, id) } /// Extract the property name from a setter function name. fn extract_prop_from_setter(fn_name: &str) -> Option<&str> { #[cfg(not(feature = "no_object"))] { - if fn_name.starts_with(FUNC_SETTER) { - Some(&fn_name[FUNC_SETTER.len()..]) + if fn_name.starts_with(FN_SET) { + Some(&fn_name[FN_SET.len()..]) } else { None } @@ -378,42 +393,72 @@ fn default_print(s: &str) { println!("{}", s); } +/// Search for a module within an imports stack. +/// Position in `EvalAltResult` is None and must be set afterwards. +fn search_imports<'s>( + mods: &'s mut Imports, + state: &mut State, + modules: &Box, +) -> Result<&'s mut Module, Box> { + let (root, root_pos) = modules.get(0); + + // Qualified - check if the root module is directly indexed + let index = if state.always_search { + None + } else { + modules.index() + }; + + Ok(if let Some(index) = index { + let offset = mods.len() - index.get(); + &mut mods.get_mut(offset).unwrap().1 + } else { + mods.iter_mut() + .rev() + .find(|(n, _)| n == root) + .map(|(_, m)| m) + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorModuleNotFound( + root.to_string(), + *root_pos, + )) + })? + }) +} + /// Search for a variable within the scope fn search_scope<'s, 'a>( scope: &'s mut Scope, + mods: &'s mut Imports, state: &mut State, + this_ptr: &'s mut Option<&mut Dynamic>, expr: &'a Expr, ) -> Result<(&'s mut Dynamic, &'a str, ScopeEntryType, Position), Box> { let ((name, pos), modules, hash_var, index) = match expr { - Expr::Variable(x) => x.as_ref(), + Expr::Variable(v) => v.as_ref(), _ => unreachable!(), }; + // Check if the variable is `this` + if name == KEYWORD_THIS { + if let Some(val) = this_ptr { + return Ok(((*val).into(), KEYWORD_THIS, ScopeEntryType::Normal, *pos)); + } else { + return Err(Box::new(EvalAltResult::ErrorUnboundedThis(*pos))); + } + } + // 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() - }; - - 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); - - scope - .find_module_internal(id) - .ok_or_else(|| Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)))? - }; - - let target = module.get_qualified_var_mut(name, *hash_var, *pos)?; + if let Some(modules) = modules { + let module = search_imports(mods, state, modules)?; + let target = module + .get_qualified_var_mut(*hash_var) + .map_err(|err| match *err { + EvalAltResult::ErrorVariableNotFound(_, _) => Box::new( + EvalAltResult::ErrorVariableNotFound(format!("{}{}", modules, name), *pos), + ), + _ => err.new_position(*pos), + })?; // Module variables are constant Ok((target, name, ScopeEntryType::Constant, *pos)) @@ -452,7 +497,7 @@ impl Engine { global_module: Default::default(), module_resolver: None, - type_names: HashMap::new(), + type_names: Default::default(), print: Box::new(|_| {}), debug: Box::new(|_| {}), progress: None, @@ -637,12 +682,14 @@ impl Engine { pub(crate) fn call_fn_raw( &self, scope: &mut Scope, + mods: &mut Imports, state: &mut State, lib: &Module, fn_name: &str, (hash_fn, hash_script): (u64, u64), args: &mut FnCallArgs, is_ref: bool, + is_method: bool, def_val: Option<&Dynamic>, level: usize, ) -> Result<(Dynamic, bool), Box> { @@ -682,10 +729,17 @@ impl Engine { // Replace the first reference with a reference to the clone, force-casting the lifetime. // Keep the original reference. Must remember to restore it later with `restore_first_arg_of_method_call`. - let this_pointer = mem::replace( - args.get_mut(0).unwrap(), - unsafe_mut_cast_to_lifetime(this_copy), - ); + // + // # Safety + // + // Blindly casting a a reference to another lifetime saves on allocations and string cloning, + // but must be used with the utmost care. + // + // We can do this here because, at the end of this scope, we'd restore the original reference + // with `restore_first_arg_of_method_call`. Therefore this shorter lifetime does not get "out". + let this_pointer = mem::replace(args.get_mut(0).unwrap(), unsafe { + mem::transmute(this_copy) + }); *old_this_ptr = Some(this_pointer); } @@ -713,9 +767,9 @@ impl Engine { .or_else(|| self.packages.get_fn(hash_fn)); if let Some(func) = func { - // Calling pure function in method-call? + // Calling pure function but the first argument is a reference? normalize_first_arg( - (func.is_pure() || func.is_script()) && is_ref, + is_ref && (func.is_pure() || (func.is_script() && !is_method)), &mut this_copy, &mut old_this_ptr, args, @@ -724,13 +778,34 @@ impl Engine { 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); + // Method call of script function - map first argument to `this` + if is_method { + let (first, rest) = args.split_at_mut(1); + return Ok(( + self.call_script_fn( + scope, + mods, + state, + lib, + &mut Some(first[0]), + fn_name, + fn_def, + rest, + level, + )?, + false, + )); + } else { + let result = self.call_script_fn( + scope, mods, state, lib, &mut None, fn_name, fn_def, args, level, + )?; - return Ok((result, false)); + // Restore the original reference + restore_first_arg(old_this_ptr, args); + + return Ok((result, false)); + }; } else { // Run external function let result = func.get_native_fn()(self, args)?; @@ -795,7 +870,7 @@ impl Engine { } // index getter function not found? - if fn_name == FUNC_INDEXER_GET && args.len() == 2 { + if fn_name == FN_IDX_GET && args.len() == 2 { return Err(Box::new(EvalAltResult::ErrorFunctionNotFound( format!( "{} [{}]", @@ -807,7 +882,7 @@ impl Engine { } // index setter function not found? - if fn_name == FUNC_INDEXER_SET { + if fn_name == FN_IDX_SET { return Err(Box::new(EvalAltResult::ErrorFunctionNotFound( format!( "{} [{}]=", @@ -847,8 +922,10 @@ impl Engine { pub(crate) fn call_script_fn( &self, scope: &mut Scope, + mods: &mut Imports, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, fn_name: &str, fn_def: &ScriptFnDef, args: &mut FnCallArgs, @@ -857,7 +934,8 @@ impl Engine { let orig_scope_level = state.scope_level; state.scope_level += 1; - let scope_len = scope.len(); + let prev_scope_len = scope.len(); + let prev_mods_len = mods.len(); // Put arguments into scope as variables // Actually consume the arguments instead of cloning them @@ -874,7 +952,7 @@ impl Engine { // Evaluate the function at one higher level of call depth let result = self - .eval_stmt(scope, state, lib, &fn_def.body, level + 1) + .eval_stmt(scope, mods, state, lib, this_ptr, &fn_def.body, level + 1) .or_else(|err| match *err { // Convert return statement to return value EvalAltResult::Return(x, _) => Ok(x), @@ -893,7 +971,8 @@ impl Engine { }); // Remove all local variables - scope.rewind(scope_len); + scope.rewind(prev_scope_len); + mods.truncate(prev_mods_len); state.scope_level = orig_scope_level; result @@ -931,6 +1010,7 @@ impl Engine { hash_script: u64, args: &mut FnCallArgs, is_ref: bool, + is_method: bool, def_val: Option<&Dynamic>, level: usize, ) -> Result<(Dynamic, bool), Box> { @@ -946,6 +1026,14 @@ impl Engine { false, )), + // Fn + KEYWORD_FN_PTR if args.len() == 1 && !self.has_override(lib, hashes) => { + Err(Box::new(EvalAltResult::ErrorRuntime( + "'Fn' should not be called in method style. Try Fn(...);".into(), + Position::none(), + ))) + } + // eval - reaching this point it must be a method-style call KEYWORD_EVAL if args.len() == 1 && !self.has_override(lib, hashes) => { Err(Box::new(EvalAltResult::ErrorRuntime( @@ -957,8 +1045,10 @@ impl Engine { // Normal function call _ => { let mut scope = Scope::new(); + let mut mods = Imports::new(); self.call_fn_raw( - &mut scope, state, lib, fn_name, hashes, args, is_ref, def_val, level, + &mut scope, &mut mods, state, lib, fn_name, hashes, args, is_ref, is_method, + def_val, level, ) } } @@ -969,6 +1059,7 @@ impl Engine { fn eval_script_expr( &self, scope: &mut Scope, + mods: &mut Imports, state: &mut State, lib: &Module, script: &Dynamic, @@ -994,7 +1085,7 @@ impl Engine { let ast = AST::new(statements, lib.clone()); // Evaluate the AST - let (result, operations) = self.eval_ast_with_scope_raw(scope, &ast)?; + let (result, operations) = self.eval_ast_with_scope_raw(scope, mods, &ast)?; state.operations += operations; self.inc_operations(state)?; @@ -1008,226 +1099,289 @@ impl Engine { &self, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, target: &mut Target, rhs: &Expr, idx_values: &mut StaticVec, - is_index: bool, + chain_type: ChainType, level: usize, mut new_val: Option, ) -> Result<(Dynamic, bool), Box> { + if chain_type == ChainType::None { + panic!(); + } + let is_ref = target.is_ref(); let is_value = target.is_value(); + let next_chain = match rhs { + Expr::Index(_) => ChainType::Index, + Expr::Dot(_) => ChainType::Dot, + _ => ChainType::None, + }; + // Pop the last index value let mut idx_val = idx_values.pop(); - if is_index { - #[cfg(feature = "no_index")] - unreachable!(); + match chain_type { + #[cfg(not(feature = "no_index"))] + ChainType::Index => { + let pos = rhs.position(); - let pos = rhs.position(); + match rhs { + // xxx[idx].expr... | xxx[idx][expr]... + Expr::Dot(x) | Expr::Index(x) => { + let (idx, expr, pos) = x.as_ref(); + let idx_pos = idx.position(); + let obj_ptr = &mut self + .get_indexed_mut(state, lib, target, idx_val, idx_pos, false, level)?; - match rhs { - // xxx[idx].expr... | xxx[idx][expr]... - Expr::Dot(x) | Expr::Index(x) => { - 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, target, idx_val, idx_pos, false)?; + self.eval_dot_index_chain_helper( + state, lib, this_ptr, obj_ptr, expr, idx_values, next_chain, level, + new_val, + ) + .map_err(|err| err.new_position(*pos)) + } + // xxx[rhs] = new_val + _ if new_val.is_some() => { + let mut idx_val2 = idx_val.clone(); - self.eval_dot_index_chain_helper( - 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 mut idx_val2 = idx_val.clone(); + match self.get_indexed_mut(state, lib, target, idx_val, pos, true, level) { + // Indexed value is an owned value - the only possibility is an indexer + // Try to call an index setter + Ok(obj_ptr) if obj_ptr.is_value() => { + let args = + &mut [target.as_mut(), &mut idx_val2, &mut new_val.unwrap()]; - 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) + self.exec_fn_call( + state, lib, FN_IDX_SET, true, 0, args, is_ref, true, None, + level, + ) .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 => + if s == FN_IDX_SET => { Ok(Default::default()) } _ => Err(err), })?; + } + // Indexed value is a reference - update directly + Ok(ref mut obj_ptr) => { + obj_ptr + .set_value(new_val.unwrap()) + .map_err(|err| err.new_position(rhs.position()))?; + } + Err(err) => match *err { + // No index getter - try to call an index setter + EvalAltResult::ErrorIndexingType(_, _) => { + let args = &mut [ + target.as_mut(), + &mut idx_val2, + &mut new_val.unwrap(), + ]; + + self.exec_fn_call( + state, lib, FN_IDX_SET, true, 0, args, is_ref, true, None, + level, + )?; + } + // Error + err => return Err(Box::new(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()]; + Ok(Default::default()) + } + // xxx[rhs] + _ => self + .get_indexed_mut(state, lib, target, idx_val, pos, false, level) + .map(|v| (v.clone_into_dynamic(), false)), + } + } + + #[cfg(not(feature = "no_object"))] + ChainType::Dot => { + match rhs { + // xxx.fn_name(arg_expr_list) + Expr::FnCall(x) if x.1.is_none() => { + let ((name, native, pos), _, hash, _, def_val) = x.as_ref(); + let def_val = def_val.as_ref(); + + // Get a reference to the mutation target Dynamic + let (result, updated) = { + let obj = target.as_mut(); + let idx = idx_val.downcast_mut::>().unwrap(); + let mut fn_name = name.as_ref(); + + // Check if it is a FnPtr call + if fn_name == KEYWORD_FN_PTR_CALL && obj.is::() { + // Redirect function name + fn_name = obj.as_str().unwrap(); + // Recalculate hash + let hash = calc_fn_hash(empty(), fn_name, idx.len(), empty()); + // Arguments are passed as-is + let mut arg_values = idx.iter_mut().collect::>(); + let args = arg_values.as_mut(); + + // Map it to name(args) in function-call style + self.exec_fn_call( + state, lib, fn_name, *native, hash, args, false, false, + def_val, level, + ) + } else { + let redirected: Option; + let mut hash = *hash; + + // Check if it is a map method call in OOP style + if let Some(map) = obj.downcast_ref::() { + if let Some(val) = map.get(fn_name) { + if let Some(f) = val.downcast_ref::() { + // Remap the function name + redirected = Some(f.get_fn_name().clone()); + fn_name = redirected.as_ref().unwrap(); + + // Recalculate the hash based on the new function name + hash = + calc_fn_hash(empty(), fn_name, idx.len(), empty()); + } + } + }; + + // Attached object pointer in front of the arguments + let mut arg_values = + once(obj).chain(idx.iter_mut()).collect::>(); + let args = arg_values.as_mut(); self.exec_fn_call( - state, lib, fn_name, true, 0, args, is_ref, None, 0, - )?; + state, lib, fn_name, *native, hash, args, is_ref, true, + def_val, level, + ) } - // Error - err => return Err(Box::new(err)), - }, + .map_err(|err| err.new_position(*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)) } - Ok(Default::default()) - } - // xxx[rhs] - _ => self - .get_indexed_mut(state, lib, target, idx_val, pos, false) - .map(|v| (v.clone_into_dynamic(), false)), - } - } else { - match rhs { - // xxx.fn_name(arg_expr_list) - Expr::FnCall(x) if x.1.is_none() => { - let ((name, native, pos), _, hash, _, def_val) = x.as_ref(); - let def_val = def_val.as_ref(); - - // 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, 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 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, target, index, *pos, true)?; - - 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 target.is::() => { - let ((prop, _, _), pos) = x.as_ref(); - let index = prop.clone().into(); - 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 = [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 = [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 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(); + // xxx.module::fn_name(...) - syntax error + Expr::FnCall(_) => unreachable!(), + // {xxx:map}.id = ??? + Expr::Property(x) if target.is::() && new_val.is_some() => { + let ((prop, _, _), pos) = x.as_ref(); let index = prop.clone().into(); - self.get_indexed_mut(state, lib, target, index, *pos, false)? - } else { - unreachable!(); - }; + let mut val = + self.get_indexed_mut(state, lib, target, index, *pos, true, level)?; - self.eval_dot_index_chain_helper( - 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 [target.as_mut(), &mut Default::default()]; + val.set_value(new_val.unwrap()) + .map_err(|err| err.new_position(rhs.position()))?; + Ok((Default::default(), true)) + } + // {xxx:map}.id + 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, target, index, *pos, false, level)?; - 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, 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, level, new_val, + 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 = [target.as_mut(), new_val.as_mut().unwrap()]; + self.exec_fn_call( + state, lib, setter, true, 0, &mut args, is_ref, true, None, level, ) - .map_err(|err| EvalAltResult::new_position(err, *pos))?; + .map(|(v, _)| (v, true)) + .map_err(|err| err.new_position(*pos)) + } + // xxx.id + Expr::Property(x) => { + let ((_, getter, _), pos) = x.as_ref(); + let mut args = [target.as_mut()]; + self.exec_fn_call( + state, lib, getter, true, 0, &mut args, is_ref, true, None, level, + ) + .map(|(v, _)| (v, false)) + .map_err(|err| err.new_position(*pos)) + } + // {xxx:map}.prop[expr] | {xxx:map}.prop.expr + Expr::Index(x) | Expr::Dot(x) if target.is::() => { + let (prop, expr, pos) = x.as_ref(); - // Feed the value back via a setter just in case it has been updated - if updated || may_be_changed { - if let Expr::Property(p) = prop { - 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, 0) + 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, target, index, *pos, false, level)? + } else { + unreachable!(); + }; + + self.eval_dot_index_chain_helper( + state, lib, this_ptr, &mut val, expr, idx_values, next_chain, level, + new_val, + ) + .map_err(|err| err.new_position(*pos)) + } + // xxx.prop[expr] | xxx.prop.expr + Expr::Index(x) | Expr::Dot(x) => { + let (prop, expr, pos) = x.as_ref(); + 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, true, None, level, + ) + .map_err(|err| err.new_position(*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, this_ptr, target, expr, idx_values, next_chain, level, + new_val, + ) + .map_err(|err| err.new_position(*pos))?; + + // Feed the value back via a setter just in case it has been updated + if updated || may_be_changed { + if let Expr::Property(p) = prop { + 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, true, None, level, + ) .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)), + _ => Err(err.new_position(*pos)), })?; + } } - } - Ok((result, may_be_changed)) + Ok((result, may_be_changed)) + } + // Syntax error + _ => Err(Box::new(EvalAltResult::ErrorDotExpr( + "".into(), + rhs.position(), + ))), } - // Syntax error - _ => Err(Box::new(EvalAltResult::ErrorDotExpr( - "".into(), - rhs.position(), - ))), } + + _ => unreachable!(), } } @@ -1235,47 +1389,53 @@ impl Engine { fn eval_dot_index_chain( &self, scope: &mut Scope, + mods: &mut Imports, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, expr: &Expr, level: usize, new_val: Option, ) -> Result> { - let ((dot_lhs, dot_rhs, op_pos), is_index) = match expr { - Expr::Index(x) => (x.as_ref(), true), - Expr::Dot(x) => (x.as_ref(), false), + let ((dot_lhs, dot_rhs, op_pos), chain_type) = match expr { + Expr::Index(x) => (x.as_ref(), ChainType::Index), + Expr::Dot(x) => (x.as_ref(), ChainType::Dot), _ => unreachable!(), }; let idx_values = &mut StaticVec::new(); - self.eval_indexed_chain(scope, state, lib, dot_rhs, idx_values, 0, level)?; + self.eval_indexed_chain( + scope, mods, state, lib, this_ptr, dot_rhs, idx_values, 0, level, + )?; match dot_lhs { // id.??? or id[???] - Expr::Variable(_) => { - let (target, name, typ, pos) = search_scope(scope, state, dot_lhs)?; + Expr::Variable(x) => { + let (var_name, var_pos) = &x.0; + self.inc_operations(state) - .map_err(|err| EvalAltResult::new_position(err, pos))?; + .map_err(|err| err.new_position(*var_pos))?; + + let (target, _, typ, pos) = search_scope(scope, mods, state, this_ptr, dot_lhs)?; // Constants cannot be modified match typ { - ScopeEntryType::Module => unreachable!(), ScopeEntryType::Constant if new_val.is_some() => { return Err(Box::new(EvalAltResult::ErrorAssignmentToConstant( - name.to_string(), + var_name.to_string(), pos, ))); } ScopeEntryType::Constant | ScopeEntryType::Normal => (), } - let this_ptr = &mut target.into(); + let obj_ptr = &mut target.into(); self.eval_dot_index_chain_helper( - state, lib, this_ptr, dot_rhs, idx_values, is_index, level, new_val, + state, lib, &mut None, obj_ptr, dot_rhs, idx_values, chain_type, level, new_val, ) .map(|(v, _)| v) - .map_err(|err| EvalAltResult::new_position(err, *op_pos)) + .map_err(|err| err.new_position(*op_pos)) } // {expr}.??? = ??? or {expr}[???] = ??? expr if new_val.is_some() => { @@ -1285,13 +1445,13 @@ impl Engine { } // {expr}.??? or {expr}[???] expr => { - let val = self.eval_expr(scope, state, lib, expr, level)?; - let this_ptr = &mut val.into(); + let val = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; + let obj_ptr = &mut val.into(); self.eval_dot_index_chain_helper( - state, lib, this_ptr, dot_rhs, idx_values, is_index, level, new_val, + state, lib, this_ptr, obj_ptr, dot_rhs, idx_values, chain_type, level, new_val, ) .map(|(v, _)| v) - .map_err(|err| EvalAltResult::new_position(err, *op_pos)) + .map_err(|err| err.new_position(*op_pos)) } } } @@ -1304,21 +1464,25 @@ impl Engine { fn eval_indexed_chain( &self, scope: &mut Scope, + mods: &mut Imports, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, expr: &Expr, idx_values: &mut StaticVec, size: usize, level: usize, ) -> Result<(), Box> { self.inc_operations(state) - .map_err(|err| EvalAltResult::new_position(err, expr.position()))?; + .map_err(|err| err.new_position(expr.position()))?; match expr { Expr::FnCall(x) if x.1.is_none() => { let arg_values = x.3.iter() - .map(|arg_expr| self.eval_expr(scope, state, lib, arg_expr, level)) + .map(|arg_expr| { + self.eval_expr(scope, mods, state, lib, this_ptr, arg_expr, level) + }) .collect::, _>>()?; idx_values.push(Dynamic::from(arg_values)); @@ -1331,15 +1495,17 @@ impl Engine { // Evaluate in left-to-right order let lhs_val = match lhs { Expr::Property(_) => Default::default(), // Store a placeholder in case of a property - _ => self.eval_expr(scope, state, lib, lhs, level)?, + _ => self.eval_expr(scope, mods, state, lib, this_ptr, lhs, level)?, }; // Push in reverse order - self.eval_indexed_chain(scope, state, lib, rhs, idx_values, size, level)?; + self.eval_indexed_chain( + scope, mods, state, lib, this_ptr, rhs, idx_values, size, level, + )?; idx_values.push(lhs_val); } - _ => idx_values.push(self.eval_expr(scope, state, lib, expr, level)?), + _ => idx_values.push(self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?), } Ok(()) @@ -1355,6 +1521,7 @@ impl Engine { mut idx: Dynamic, idx_pos: Position, create: bool, + level: usize, ) -> Result, Box> { self.inc_operations(state)?; @@ -1389,7 +1556,7 @@ impl Engine { // val_map[idx] Ok(if create { let index = idx - .take_string() + .take_immutable_string() .map_err(|_| EvalAltResult::ErrorStringIndexExpr(idx_pos))?; map.entry(index).or_insert(Default::default()).into() @@ -1398,7 +1565,7 @@ impl Engine { .downcast_ref::() .ok_or_else(|| EvalAltResult::ErrorStringIndexExpr(idx_pos))?; - map.get_mut(index) + map.get_mut(index.as_str()) .map(Target::from) .unwrap_or_else(|| Target::from(())) }) @@ -1427,17 +1594,18 @@ impl Engine { #[cfg(not(feature = "no_index"))] _ => { - 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, 0) - .map(|(v, _)| v.into()) - .map_err(|_| { - Box::new(EvalAltResult::ErrorIndexingType( - type_name.into(), - Position::none(), - )) - }) + self.exec_fn_call( + state, lib, FN_IDX_GET, true, 0, args, is_ref, true, None, level, + ) + .map(|(v, _)| v.into()) + .map_err(|_| { + Box::new(EvalAltResult::ErrorIndexingType( + type_name.into(), + Position::none(), + )) + }) } #[cfg(feature = "no_index")] @@ -1452,17 +1620,19 @@ impl Engine { fn eval_in_expr( &self, scope: &mut Scope, + mods: &mut Imports, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, lhs: &Expr, rhs: &Expr, level: usize, ) -> Result> { self.inc_operations(state) - .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; + .map_err(|err| err.new_position(rhs.position()))?; - let lhs_value = self.eval_expr(scope, state, lib, lhs, level)?; - let rhs_value = self.eval_expr(scope, state, lib, rhs, level)?; + let lhs_value = self.eval_expr(scope, mods, state, lib, this_ptr, lhs, level)?; + let rhs_value = self.eval_expr(scope, mods, state, lib, this_ptr, rhs, level)?; match rhs_value { #[cfg(not(feature = "no_index"))] @@ -1484,9 +1654,10 @@ impl Engine { let (r, _) = self .call_fn_raw( - &mut scope, state, lib, op, hashes, args, false, def_value, level, + &mut scope, mods, state, lib, op, hashes, args, false, false, + def_value, level, ) - .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; + .map_err(|err| err.new_position(rhs.position()))?; if r.as_bool().unwrap_or(false) { return Ok(true.into()); } @@ -1498,7 +1669,9 @@ impl Engine { Dynamic(Union::Map(rhs_value)) => match lhs_value { // Only allows String or char Dynamic(Union::Str(s)) => Ok(rhs_value.contains_key(s.as_str()).into()), - Dynamic(Union::Char(c)) => Ok(rhs_value.contains_key(&c.to_string()).into()), + Dynamic(Union::Char(c)) => { + Ok(rhs_value.contains_key(c.to_string().as_str()).into()) + } _ => Err(Box::new(EvalAltResult::ErrorInExpr(lhs.position()))), }, Dynamic(Union::Str(rhs_value)) => match lhs_value { @@ -1515,38 +1688,49 @@ impl Engine { fn eval_expr( &self, scope: &mut Scope, + mods: &mut Imports, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, expr: &Expr, level: usize, ) -> Result> { self.inc_operations(state) - .map_err(|err| EvalAltResult::new_position(err, expr.position()))?; + .map_err(|err| err.new_position(expr.position()))?; let result = match expr { - Expr::Expr(x) => self.eval_expr(scope, state, lib, x.as_ref(), level), + Expr::Expr(x) => self.eval_expr(scope, mods, state, lib, this_ptr, x.as_ref(), level), Expr::IntegerConstant(x) => Ok(x.0.into()), #[cfg(not(feature = "no_float"))] Expr::FloatConstant(x) => Ok(x.0.into()), Expr::StringConstant(x) => Ok(x.0.to_string().into()), Expr::CharConstant(x) => Ok(x.0.into()), + Expr::Variable(x) if (x.0).0 == KEYWORD_THIS => { + if let Some(ref val) = this_ptr { + Ok((*val).clone()) + } else { + Err(Box::new(EvalAltResult::ErrorUnboundedThis((x.0).1))) + } + } Expr::Variable(_) => { - let (val, _, _, _) = search_scope(scope, state, expr)?; + let (val, _, _, _) = search_scope(scope, mods, state, this_ptr, expr)?; Ok(val.clone()) } Expr::Property(_) => unreachable!(), // Statement block - Expr::Stmt(x) => self.eval_stmt(scope, state, lib, &x.0, level), + Expr::Stmt(x) => self.eval_stmt(scope, mods, state, lib, this_ptr, &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)?; + let mut rhs_val = + self.eval_expr(scope, mods, state, lib, this_ptr, rhs_expr, level)?; + let (lhs_ptr, name, typ, pos) = + search_scope(scope, mods, state, this_ptr, lhs_expr)?; self.inc_operations(state) - .map_err(|err| EvalAltResult::new_position(err, pos))?; + .map_err(|err| err.new_position(pos))?; match typ { // Assignment to constant variable @@ -1583,21 +1767,22 @@ impl Engine { // Set variable value *lhs_ptr = self - .exec_fn_call(state, lib, op, true, hash, args, false, None, level) + .exec_fn_call( + state, lib, op, true, hash, args, false, false, None, level, + ) .map(|(v, _)| v) - .map_err(|err| EvalAltResult::new_position(err, *op_pos))?; + .map_err(|err| err.new_position(*op_pos))?; } Ok(Default::default()) } - // A module cannot be assigned to - ScopeEntryType::Module => unreachable!(), } } // lhs op= rhs Expr::Assignment(x) => { 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 mut rhs_val = + self.eval_expr(scope, mods, state, lib, this_ptr, rhs_expr, level)?; let new_val = Some(if op.is_empty() { // Normal assignment @@ -1607,12 +1792,12 @@ impl Engine { let op = &op[..op.len() - 1]; // extract operator without = let hash = calc_fn_hash(empty(), op, 2, empty()); let args = &mut [ - &mut self.eval_expr(scope, state, lib, lhs_expr, level)?, + &mut self.eval_expr(scope, mods, state, lib, this_ptr, lhs_expr, level)?, &mut rhs_val, ]; - self.exec_fn_call(state, lib, op, true, hash, args, false, None, level) + self.exec_fn_call(state, lib, op, true, hash, args, false, false, None, level) .map(|(v, _)| v) - .map_err(|err| EvalAltResult::new_position(err, *op_pos))? + .map_err(|err| err.new_position(*op_pos))? }); match lhs_expr { @@ -1620,14 +1805,14 @@ impl Engine { Expr::Variable(_) => unreachable!(), // idx_lhs[idx_expr] op= rhs #[cfg(not(feature = "no_index"))] - Expr::Index(_) => { - self.eval_dot_index_chain(scope, state, lib, lhs_expr, level, new_val) - } + Expr::Index(_) => self.eval_dot_index_chain( + scope, mods, state, lib, this_ptr, lhs_expr, level, new_val, + ), // dot_lhs.dot_rhs op= rhs #[cfg(not(feature = "no_object"))] - Expr::Dot(_) => { - self.eval_dot_index_chain(scope, state, lib, lhs_expr, level, new_val) - } + Expr::Dot(_) => self.eval_dot_index_chain( + scope, mods, state, lib, this_ptr, lhs_expr, level, new_val, + ), // Error assignment to constant expr if expr.is_constant() => { Err(Box::new(EvalAltResult::ErrorAssignmentToConstant( @@ -1644,16 +1829,20 @@ impl Engine { // lhs[idx_expr] #[cfg(not(feature = "no_index"))] - Expr::Index(_) => self.eval_dot_index_chain(scope, state, lib, expr, level, None), + Expr::Index(_) => { + self.eval_dot_index_chain(scope, mods, state, lib, this_ptr, expr, level, None) + } // lhs.dot_rhs #[cfg(not(feature = "no_object"))] - Expr::Dot(_) => self.eval_dot_index_chain(scope, state, lib, expr, level, None), + Expr::Dot(_) => { + self.eval_dot_index_chain(scope, mods, state, lib, this_ptr, expr, level, None) + } #[cfg(not(feature = "no_index"))] Expr::Array(x) => Ok(Dynamic(Union::Array(Box::new( x.0.iter() - .map(|item| self.eval_expr(scope, state, lib, item, level)) + .map(|item| self.eval_expr(scope, mods, state, lib, this_ptr, item, level)) .collect::, _>>()?, )))), @@ -1661,7 +1850,7 @@ impl Engine { Expr::Map(x) => Ok(Dynamic(Union::Map(Box::new( x.0.iter() .map(|((key, _), expr)| { - self.eval_expr(scope, state, lib, expr, level) + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) .map(|val| (key.clone(), val)) }) .collect::, _>>()?, @@ -1672,7 +1861,39 @@ impl Engine { let ((name, native, pos), _, hash, args_expr, def_val) = x.as_ref(); let def_val = def_val.as_ref(); - // Handle eval + // Handle Fn() + if name == KEYWORD_FN_PTR && args_expr.len() == 1 { + let hash_fn = + calc_fn_hash(empty(), name, 1, once(TypeId::of::())); + + if !self.has_override(lib, (hash_fn, *hash)) { + // Fn - only in function call style + let expr = args_expr.get(0); + let arg_value = + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; + return arg_value + .take_immutable_string() + .map_err(|type_name| { + Box::new(EvalAltResult::ErrorMismatchOutputType( + type_name.into(), + expr.position(), + )) + }) + .and_then(|s| { + if is_valid_identifier(s.chars()) { + Ok(s) + } else { + Err(Box::new(EvalAltResult::ErrorFunctionNotFound( + s.to_string(), + expr.position(), + ))) + } + }) + .map(|s| FnPtr::from(s).into()); + } + } + + // Handle eval() if name == KEYWORD_EVAL && args_expr.len() == 1 { let hash_fn = calc_fn_hash(empty(), name, 1, once(TypeId::of::())); @@ -1681,10 +1902,11 @@ impl Engine { // eval - only in function call style let prev_len = scope.len(); let expr = args_expr.get(0); - let script = self.eval_expr(scope, state, lib, expr, level)?; + let script = + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; let result = self - .eval_script_expr(scope, state, lib, &script) - .map_err(|err| EvalAltResult::new_position(err, expr.position())); + .eval_script_expr(scope, mods, state, lib, &script) + .map_err(|err| err.new_position(expr.position())); if scope.len() != prev_len { // IMPORTANT! If the eval defines new variables in the current scope, @@ -1696,7 +1918,7 @@ impl Engine { } } - // Normal function call - except for eval (handled above) + // Normal function call - except for Fn and eval (handled above) let mut arg_values: StaticVec; let mut args: StaticVec<_>; let mut is_ref = false; @@ -1713,17 +1935,16 @@ impl Engine { arg_values = args_expr .iter() .skip(1) - .map(|expr| self.eval_expr(scope, state, lib, expr, level)) + .map(|expr| { + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) + }) .collect::>()?; - let (target, _, typ, pos) = search_scope(scope, state, lhs)?; - self.inc_operations(state) - .map_err(|err| EvalAltResult::new_position(err, pos))?; + let (target, _, _, pos) = + search_scope(scope, mods, state, this_ptr, lhs)?; - match typ { - ScopeEntryType::Module => unreachable!(), - ScopeEntryType::Constant | ScopeEntryType::Normal => (), - } + self.inc_operations(state) + .map_err(|err| err.new_position(pos))?; args = once(target).chain(arg_values.iter_mut()).collect(); @@ -1733,7 +1954,9 @@ impl Engine { _ => { arg_values = args_expr .iter() - .map(|expr| self.eval_expr(scope, state, lib, expr, level)) + .map(|expr| { + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) + }) .collect::>()?; args = arg_values.iter_mut().collect(); @@ -1743,10 +1966,10 @@ impl Engine { let args = args.as_mut(); self.exec_fn_call( - state, lib, name, *native, *hash, args, is_ref, def_val, level, + state, lib, name, *native, *hash, args, is_ref, false, def_val, level, ) .map(|(v, _)| v) - .map_err(|err| EvalAltResult::new_position(err, *pos)) + .map_err(|err| err.new_position(*pos)) } // Module-qualified function call @@ -1756,31 +1979,19 @@ impl Engine { let mut arg_values = args_expr .iter() - .map(|expr| self.eval_expr(scope, state, lib, expr, level)) + .map(|expr| self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)) .collect::, _>>()?; let mut args: StaticVec<_> = arg_values.iter_mut().collect(); - let (id, root_pos) = modules.get(0); // First module - - let module = if let Some(index) = modules.index() { - scope - .get_mut(scope.len() - index.get()) - .0 - .downcast_mut::() - .unwrap() - } else { - scope.find_module_internal(id).ok_or_else(|| { - Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)) - })? - }; + let module = search_imports(mods, state, modules)?; // First search in script-defined functions (can override built-in) - let func = match module.get_qualified_fn(name, *hash_script) { + let func = match module.get_qualified_fn(*hash_script) { Err(err) if matches!(*err, EvalAltResult::ErrorFunctionNotFound(_, _)) => { // Then search in Rust functions self.inc_operations(state) - .map_err(|err| EvalAltResult::new_position(err, *pos))?; + .map_err(|err| err.new_position(*pos))?; // Qualified Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, @@ -1792,7 +2003,7 @@ impl Engine { // 3) The final hash is the XOR of the two hashes. let hash_qualified_fn = *hash_script ^ hash_fn_args; - module.get_qualified_fn(name, hash_qualified_fn) + module.get_qualified_fn(hash_qualified_fn) } r => r, }; @@ -1802,35 +2013,43 @@ impl Engine { let args = args.as_mut(); let fn_def = f.get_fn_def(); let mut scope = Scope::new(); - self.call_script_fn(&mut scope, state, lib, name, fn_def, args, level) - .map_err(|err| EvalAltResult::new_position(err, *pos)) + let mut mods = Imports::new(); + self.call_script_fn( + &mut scope, &mut mods, state, lib, &mut None, name, fn_def, args, level, + ) + .map_err(|err| err.new_position(*pos)) } Ok(f) => { f.get_native_fn()(self, args.as_mut()).map_err(|err| err.new_position(*pos)) } - Err(err) - if def_val.is_some() - && matches!(*err, EvalAltResult::ErrorFunctionNotFound(_, _)) => - { - Ok(def_val.clone().unwrap()) - } - Err(err) => Err(err), + Err(err) => match *err { + EvalAltResult::ErrorFunctionNotFound(_, _) if def_val.is_some() => { + Ok(def_val.clone().unwrap()) + } + EvalAltResult::ErrorFunctionNotFound(_, _) => { + Err(Box::new(EvalAltResult::ErrorFunctionNotFound( + format!("{}{}", modules, name), + *pos, + ))) + } + _ => Err(err.new_position(*pos)), + }, } } - Expr::In(x) => self.eval_in_expr(scope, state, lib, &x.0, &x.1, level), + Expr::In(x) => self.eval_in_expr(scope, mods, state, lib, this_ptr, &x.0, &x.1, level), Expr::And(x) => { let (lhs, rhs, _) = x.as_ref(); Ok((self - .eval_expr(scope, state, lib, lhs, level)? + .eval_expr(scope, mods, state, lib, this_ptr, lhs, level)? .as_bool() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("AND".into(), lhs.position()) })? && // Short-circuit using && self - .eval_expr(scope, state, lib, rhs, level)? + .eval_expr(scope, mods, state, lib, this_ptr, rhs, level)? .as_bool() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("AND".into(), rhs.position()) @@ -1841,14 +2060,14 @@ impl Engine { Expr::Or(x) => { let (lhs, rhs, _) = x.as_ref(); Ok((self - .eval_expr(scope, state, lib, lhs, level)? + .eval_expr(scope, mods, state, lib, this_ptr, lhs, level)? .as_bool() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("OR".into(), lhs.position()) })? || // Short-circuit using || self - .eval_expr(scope, state, lib, rhs, level)? + .eval_expr(scope, mods, state, lib, this_ptr, rhs, level)? .as_bool() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("OR".into(), rhs.position()) @@ -1864,19 +2083,22 @@ impl Engine { }; self.check_data_size(result) + .map_err(|err| err.new_position(expr.position())) } /// Evaluate a statement pub(crate) fn eval_stmt( &self, scope: &mut Scope, + mods: &mut Imports, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, stmt: &Stmt, level: usize, ) -> Result> { self.inc_operations(state) - .map_err(|err| EvalAltResult::new_position(err, stmt.position()))?; + .map_err(|err| err.new_position(stmt.position()))?; let result = match stmt { // No-op @@ -1884,7 +2106,7 @@ impl Engine { // Expression as statement Stmt::Expr(expr) => { - let result = self.eval_expr(scope, state, lib, expr, level)?; + let result = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; Ok(match expr.as_ref() { // If it is a simple assignment, erase the result at the root @@ -1895,14 +2117,16 @@ impl Engine { // Block scope Stmt::Block(x) => { - let prev_len = scope.len(); + let prev_scope_len = scope.len(); + let prev_mods_len = mods.len(); state.scope_level += 1; let result = x.0.iter().try_fold(Default::default(), |_, stmt| { - self.eval_stmt(scope, state, lib, stmt, level) + self.eval_stmt(scope, mods, state, lib, this_ptr, stmt, level) }); - scope.rewind(prev_len); + scope.rewind(prev_scope_len); + mods.truncate(prev_mods_len); state.scope_level -= 1; // The impact of an eval statement goes away at the end of a block @@ -1916,14 +2140,14 @@ impl Engine { Stmt::IfThenElse(x) => { let (expr, if_block, else_block) = x.as_ref(); - self.eval_expr(scope, state, lib, expr, level)? + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)? .as_bool() .map_err(|_| Box::new(EvalAltResult::ErrorLogicGuard(expr.position()))) .and_then(|guard_val| { if guard_val { - self.eval_stmt(scope, state, lib, if_block, level) + self.eval_stmt(scope, mods, state, lib, this_ptr, if_block, level) } else if let Some(stmt) = else_block { - self.eval_stmt(scope, state, lib, stmt, level) + self.eval_stmt(scope, mods, state, lib, this_ptr, stmt, level) } else { Ok(Default::default()) } @@ -1934,15 +2158,22 @@ impl Engine { Stmt::While(x) => loop { let (expr, body) = x.as_ref(); - match self.eval_expr(scope, state, lib, expr, level)?.as_bool() { - Ok(true) => match self.eval_stmt(scope, state, lib, body, level) { - Ok(_) => (), - Err(err) => match *err { - EvalAltResult::ErrorLoopBreak(false, _) => (), - EvalAltResult::ErrorLoopBreak(true, _) => return Ok(Default::default()), - _ => return Err(err), - }, - }, + match self + .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? + .as_bool() + { + Ok(true) => { + match self.eval_stmt(scope, mods, state, lib, this_ptr, body, level) { + Ok(_) => (), + Err(err) => match *err { + EvalAltResult::ErrorLoopBreak(false, _) => (), + EvalAltResult::ErrorLoopBreak(true, _) => { + return Ok(Default::default()) + } + _ => return Err(err), + }, + } + } Ok(false) => return Ok(Default::default()), Err(_) => { return Err(Box::new(EvalAltResult::ErrorLogicGuard(expr.position()))) @@ -1952,7 +2183,7 @@ impl Engine { // Loop statement Stmt::Loop(body) => loop { - match self.eval_stmt(scope, state, lib, body, level) { + match self.eval_stmt(scope, mods, state, lib, this_ptr, body, level) { Ok(_) => (), Err(err) => match *err { EvalAltResult::ErrorLoopBreak(false, _) => (), @@ -1965,7 +2196,7 @@ impl Engine { // For loop Stmt::For(x) => { let (name, expr, stmt) = x.as_ref(); - let iter_type = self.eval_expr(scope, state, lib, expr, level)?; + let iter_type = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; let tid = iter_type.type_id(); if let Some(func) = self @@ -1982,9 +2213,9 @@ impl Engine { for loop_var in func(iter_type) { *scope.get_mut(index).0 = loop_var; self.inc_operations(state) - .map_err(|err| EvalAltResult::new_position(err, stmt.position()))?; + .map_err(|err| err.new_position(stmt.position()))?; - match self.eval_stmt(scope, state, lib, stmt, level) { + match self.eval_stmt(scope, mods, state, lib, this_ptr, stmt, level) { Ok(_) => (), Err(err) => match *err { EvalAltResult::ErrorLoopBreak(false, _) => (), @@ -2011,7 +2242,15 @@ impl Engine { // Return value Stmt::ReturnWithVal(x) if x.1.is_some() && (x.0).0 == ReturnType::Return => { Err(Box::new(EvalAltResult::Return( - self.eval_expr(scope, state, lib, x.1.as_ref().unwrap(), level)?, + self.eval_expr( + scope, + mods, + state, + lib, + this_ptr, + x.1.as_ref().unwrap(), + level, + )?, (x.0).1, ))) } @@ -2023,7 +2262,15 @@ impl Engine { // Throw value Stmt::ReturnWithVal(x) if x.1.is_some() && (x.0).0 == ReturnType::Exception => { - let val = self.eval_expr(scope, state, lib, x.1.as_ref().unwrap(), level)?; + let val = self.eval_expr( + scope, + mods, + state, + lib, + this_ptr, + x.1.as_ref().unwrap(), + level, + )?; Err(Box::new(EvalAltResult::ErrorRuntime( val.take_string().unwrap_or_else(|_| "".into()), (x.0).1, @@ -2040,7 +2287,15 @@ impl Engine { // Let statement Stmt::Let(x) if x.1.is_some() => { let ((var_name, _), expr) = x.as_ref(); - let val = self.eval_expr(scope, state, lib, expr.as_ref().unwrap(), level)?; + let val = self.eval_expr( + scope, + mods, + state, + lib, + this_ptr, + expr.as_ref().unwrap(), + level, + )?; let var_name = unsafe_cast_var_name_to_lifetime(var_name, &state); scope.push_dynamic_value(var_name, ScopeEntryType::Normal, val, false); Ok(Default::default()) @@ -2056,7 +2311,7 @@ impl Engine { // Const statement Stmt::Const(x) if x.1.is_constant() => { let ((var_name, _), expr) = x.as_ref(); - let val = self.eval_expr(scope, state, lib, &expr, level)?; + let val = self.eval_expr(scope, mods, state, lib, this_ptr, &expr, level)?; let var_name = unsafe_cast_var_name_to_lifetime(var_name, &state); scope.push_dynamic_value(var_name, ScopeEntryType::Constant, val, true); Ok(Default::default()) @@ -2075,15 +2330,15 @@ impl Engine { } if let Some(path) = self - .eval_expr(scope, state, lib, &expr, level)? + .eval_expr(scope, mods, state, lib, this_ptr, &expr, level)? .try_cast::() { #[cfg(not(feature = "no_module"))] { if let Some(resolver) = &self.module_resolver { - let module = resolver.resolve(self, &path, expr.position())?; - let mod_name = unsafe_cast_var_name_to_lifetime(name, &state); - scope.push_module_internal(mod_name, module); + let mut module = resolver.resolve(self, &path, expr.position())?; + module.index_all_sub_modules(); + mods.push((name.clone().into(), module)); state.modules += 1; @@ -2107,11 +2362,7 @@ impl Engine { Stmt::Export(list) => { for ((id, id_pos), rename) in list.iter() { // Mark scope variables as public - if let Some(index) = scope - .get_index(id) - .map(|(i, _)| i) - .or_else(|| scope.get_module_index(id)) - { + if let Some(index) = scope.get_index(id).map(|(i, _)| i) { let alias = rename .as_ref() .map(|(n, _)| n.clone()) @@ -2130,9 +2381,11 @@ impl Engine { }; self.check_data_size(result) + .map_err(|err| err.new_position(stmt.position())) } /// Check a result to ensure that the data size is within allowable limit. + /// Position in `EvalAltResult` may be None and should be set afterwards. fn check_data_size( &self, result: Result>, diff --git a/src/error.rs b/src/error.rs index 16ea36bd..58423ef9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,10 +3,17 @@ use crate::result::EvalAltResult; use crate::token::Position; -use crate::stdlib::{boxed::Box, char, error::Error, fmt, string::{String, ToString}}; +use crate::stdlib::{ + boxed::Box, + char, + error::Error, + fmt, + string::{String, ToString}, +}; /// Error when tokenizing the script text. #[derive(Debug, Eq, PartialEq, Clone, Hash)] +#[non_exhaustive] pub enum LexError { /// An unexpected character is encountered when tokenizing the script text. UnexpectedChar(char), @@ -60,6 +67,7 @@ impl LexError { /// They still exist so that the application can turn features on and off without going through /// massive code changes to remove/add back enum variants in match statements. #[derive(Debug, Eq, PartialEq, Clone, Hash)] +#[non_exhaustive] pub enum ParseErrorType { /// Error in the script text. Wrapped value is the error message. BadInput(String), diff --git a/src/fn_func.rs b/src/fn_func.rs index bbbab4aa..ccb861a6 100644 --- a/src/fn_func.rs +++ b/src/fn_func.rs @@ -15,8 +15,8 @@ use crate::stdlib::{boxed::Box, string::ToString}; pub trait Func { type Output; - /// Create a Rust anonymous function from an `AST`. - /// The `Engine` and `AST` are consumed and basically embedded into the closure. + /// Create a Rust anonymous function from an [`AST`]. + /// The `Engine` and [`AST`] are consumed and basically embedded into the closure. /// /// # Examples /// diff --git a/src/fn_native.rs b/src/fn_native.rs index b2b3aa67..e41ae7db 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -2,6 +2,7 @@ use crate::any::Dynamic; use crate::engine::Engine; use crate::parser::ScriptFnDef; use crate::result::EvalAltResult; +use crate::utils::ImmutableString; use crate::stdlib::{boxed::Box, fmt, rc::Rc, sync::Arc}; @@ -53,8 +54,41 @@ pub fn shared_take(value: Shared) -> T { pub type FnCallArgs<'a> = [&'a mut Dynamic]; +/// A general function pointer. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] +pub struct FnPtr(ImmutableString); + +impl FnPtr { + /// Get the name of the function. + pub fn fn_name(&self) -> &str { + self.get_fn_name().as_ref() + } + /// Get the name of the function. + pub(crate) fn get_fn_name(&self) -> &ImmutableString { + &self.0 + } + /// Get the name of the function. + pub(crate) fn take_fn_name(self) -> ImmutableString { + self.0 + } +} + +impl fmt::Display for FnPtr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Fn({})", self.0) + } +} + +impl> From for FnPtr { + fn from(value: S) -> Self { + Self(value.into()) + } +} + +/// A general function trail object. #[cfg(not(feature = "sync"))] pub type FnAny = dyn Fn(&Engine, &mut FnCallArgs) -> Result>; +/// A general function trail object. #[cfg(feature = "sync")] pub type FnAny = dyn Fn(&Engine, &mut FnCallArgs) -> Result> + Send + Sync; @@ -94,6 +128,17 @@ impl fmt::Debug for CallableFunction { } } +impl fmt::Display 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"), + CallableFunction::Script(s) => fmt::Display::fmt(s, f), + } + } +} + impl CallableFunction { /// Is this a pure native Rust function? pub fn is_pure(&self) -> bool { @@ -102,7 +147,7 @@ impl CallableFunction { Self::Method(_) | Self::Iterator(_) | Self::Script(_) => false, } } - /// Is this a pure native Rust method-call? + /// Is this a native Rust method function? pub fn is_method(&self) -> bool { match self { Self::Method(_) => true, diff --git a/src/lib.rs b/src/lib.rs index 648a8dce..aa205dec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ //! //! Rhai is a tiny, simple and very fast embedded scripting language for Rust //! that gives you a safe and easy way to add scripting to your applications. -//! It provides a familiar syntax based on JS and Rust and a simple Rust interface. +//! It provides a familiar syntax based on JavaScript and Rust and a simple Rust interface. //! Here is a quick example. //! //! First, the contents of `my_script.rhai`: @@ -62,9 +62,10 @@ //! | `only_i32` | Set the system integer type to `i32` and disable all other integer types. `INT` is set to `i32`. | //! | `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. | //! | `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | -//! | `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, `Engine`, `Scope` and `AST` are all `Send + Sync`. | +//! | `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, `Engine`, `Scope` and [`AST`] are all `Send + Sync`. | +//! | `internals` | Expose internal data structures (beware they may be volatile from version to version). | //! -//! See [The Rhai Book](https://schungx.github.io/rhai/) for details on the Rhai script engine and language. +//! See [The Rhai Book](https://schungx.github.io/rhai) for details on the Rhai script engine and language. #![cfg_attr(feature = "no_std", no_std)] @@ -102,6 +103,9 @@ pub use scope::Scope; pub use token::Position; pub use utils::calc_fn_spec as calc_fn_hash; +#[cfg(not(feature = "no_function"))] +pub use parser::FnAccess; + #[cfg(not(feature = "no_function"))] pub use fn_func::Func; @@ -125,3 +129,21 @@ pub mod module_resolvers { #[cfg(not(feature = "no_optimize"))] pub use optimize::OptimizationLevel; + +// Expose internal data structures. + +#[cfg(feature = "internals")] +#[deprecated(note = "this type is volatile and may change")] +pub use token::{get_next_token, parse_string_literal, InputStream, Token, TokenizeState}; + +#[cfg(feature = "internals")] +#[deprecated(note = "this type is volatile and may change")] +pub use parser::{Expr, ReturnType, ScriptFnDef, Stmt}; + +#[cfg(feature = "internals")] +#[deprecated(note = "this type is volatile and may change")] +pub use module::ModuleRef; + +#[cfg(feature = "internals")] +#[deprecated(note = "this type is volatile and may change")] +pub use utils::StaticVec; diff --git a/src/module.rs b/src/module.rs index 24e51f68..3cdeb711 100644 --- a/src/module.rs +++ b/src/module.rs @@ -2,28 +2,30 @@ use crate::any::{Dynamic, Variant}; use crate::calc_fn_hash; -use crate::engine::{make_getter, make_setter, Engine, FUNC_INDEXER_GET, FUNC_INDEXER_SET}; -use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync}; +use crate::engine::{make_getter, make_setter, Engine, Imports, FN_IDX_GET, FN_IDX_SET}; +use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync, Shared}; use crate::parser::{ FnAccess, FnAccess::{Private, Public}, ScriptFnDef, AST, }; use crate::result::EvalAltResult; -use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; +use crate::scope::{Entry as ScopeEntry, Scope}; use crate::token::{Position, Token}; use crate::utils::{StaticVec, StraightHasherBuilder}; use crate::stdlib::{ any::TypeId, boxed::Box, + cell::RefCell, collections::HashMap, - fmt, + fmt, format, iter::empty, mem, num::NonZeroUsize, ops::{Deref, DerefMut}, string::{String, ToString}, + sync::RwLock, vec, vec::Vec, }; @@ -35,7 +37,7 @@ pub type FuncReturn = Result>; /// external Rust functions, and script-defined functions. /// /// Not available under the `no_module` feature. -#[derive(Clone, Default)] +#[derive(Default)] pub struct Module { /// Sub-modules. modules: HashMap, @@ -59,19 +61,47 @@ pub struct Module { /// Flattened collection of all external Rust functions, native or scripted, /// including those in sub-modules. all_functions: HashMap, + + /// Is the module indexed? + indexed: bool, } impl fmt::Debug for Module { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "", - self.variables, - self.functions.len(), + "Module(\n modules: {}\n vars: {}\n functions: {}\n)", + self.modules + .keys() + .map(|k| k.as_str()) + .collect::>() + .join(", "), + self.variables + .iter() + .map(|(k, v)| format!("{}={:?}", k, v)) + .collect::>() + .join(", "), + self.functions + .values() + .map(|(_, _, _, f)| f.to_string()) + .collect::>() + .join(", "), ) } } +impl Clone for Module { + fn clone(&self) -> Self { + // Only clone the index at the top level + Self { + all_variables: self.all_variables.clone(), + all_functions: self.all_functions.clone(), + indexed: self.indexed, + ..self.do_clone(false) + } + } +} + impl Module { /// Create a new module. /// @@ -106,6 +136,24 @@ impl Module { } } + /// Clone the module, optionally skipping the index. + fn do_clone(&self, clone_index: bool) -> Self { + Self { + modules: if clone_index { + self.modules.clone() + } else { + self.modules + .iter() + .map(|(k, m)| (k.clone(), m.do_clone(clone_index))) + .collect() + }, + variables: self.variables.clone(), + functions: self.functions.clone(), + type_iterators: self.type_iterators.clone(), + ..Default::default() + } + } + /// Does a variable exist in the module? /// /// # Examples @@ -166,20 +214,23 @@ impl Module { /// ``` pub fn set_var(&mut self, name: impl Into, value: impl Variant + Clone) { self.variables.insert(name.into(), Dynamic::from(value)); + self.indexed = false; } /// Get a mutable reference to a modules-qualified variable. + /// Name and Position in `EvalAltResult` are None and must be set afterwards. /// /// The `u64` hash is calculated by the function `crate::calc_fn_hash`. pub(crate) fn get_qualified_var_mut( &mut self, - name: &str, hash_var: u64, - pos: Position, ) -> Result<&mut Dynamic, Box> { - self.all_variables - .get_mut(&hash_var) - .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.to_string(), pos))) + self.all_variables.get_mut(&hash_var).ok_or_else(|| { + Box::new(EvalAltResult::ErrorVariableNotFound( + String::new(), + Position::none(), + )) + }) } /// Set a script-defined function into the module. @@ -197,6 +248,7 @@ impl Module { fn_def.into(), ), ); + self.indexed = false; } /// Does a sub-module exist in the module? @@ -263,6 +315,7 @@ impl Module { /// ``` pub fn set_sub_module(&mut self, name: impl Into, sub_module: Module) { self.modules.insert(name.into(), sub_module.into()); + self.indexed = false; } /// Does the particular Rust function exist in the module? @@ -302,6 +355,8 @@ impl Module { self.functions .insert(hash_fn, (name, access, params, func.into())); + self.indexed = false; + hash_fn } @@ -557,7 +612,7 @@ impl Module { &mut self, func: impl Fn(&mut A, B) -> FuncReturn + SendSync + 'static, ) -> u64 { - self.set_fn_2_mut(FUNC_INDEXER_GET, func) + self.set_fn_2_mut(FN_IDX_GET, func) } /// Set a Rust function taking three parameters into the module, returning a hash key. @@ -673,7 +728,7 @@ impl Module { }; let args = [TypeId::of::(), TypeId::of::(), TypeId::of::()]; self.set_fn( - FUNC_INDEXER_SET, + FN_IDX_SET, Public, &args, CallableFunction::from_method(Box::new(f)), @@ -786,17 +841,17 @@ impl Module { } /// Get a modules-qualified function. + /// Name and Position in `EvalAltResult` are None and must be set afterwards. /// /// The `u64` hash is calculated by the function `crate::calc_fn_hash` and must match /// the hash calculated by `index_all_sub_modules`. pub(crate) fn get_qualified_fn( &mut self, - name: &str, hash_qualified_fn: u64, ) -> Result<&CallableFunction, Box> { self.all_functions.get(&hash_qualified_fn).ok_or_else(|| { Box::new(EvalAltResult::ErrorFunctionNotFound( - name.to_string(), + String::new(), Position::none(), )) }) @@ -804,12 +859,53 @@ impl Module { /// Merge another module into this module. pub fn merge(&mut self, other: &Self) { + self.merge_filtered(other, |_, _, _| true) + } + + /// Merge another module into this module, with only selected functions based on a filter predicate. + pub(crate) fn merge_filtered( + &mut self, + other: &Self, + filter: impl Fn(FnAccess, &str, usize) -> bool, + ) { 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.functions.extend( + other + .functions + .iter() + .filter(|(_, (_, _, _, v))| match v { + CallableFunction::Pure(_) + | CallableFunction::Method(_) + | CallableFunction::Iterator(_) => true, + CallableFunction::Script(ref f) => { + filter(f.access, f.name.as_str(), f.params.len()) + } + }) + .map(|(&k, v)| (k, v.clone())), + ); + self.type_iterators .extend(other.type_iterators.iter().map(|(&k, v)| (k, v.clone()))); + + self.all_functions.clear(); + self.all_variables.clear(); + self.indexed = false; + } + + /// Filter out the functions, retaining only some based on a filter predicate. + pub(crate) fn retain_functions(&mut self, filter: impl Fn(FnAccess, &str, usize) -> bool) { + self.functions.retain(|_, (_, _, _, v)| match v { + CallableFunction::Pure(_) + | CallableFunction::Method(_) + | CallableFunction::Iterator(_) => true, + CallableFunction::Script(ref f) => filter(f.access, f.name.as_str(), f.params.len()), + }); + + self.all_functions.clear(); + self.all_variables.clear(); + self.indexed = false; } /// Get the number of variables in the module. @@ -837,7 +933,16 @@ impl Module { self.functions.values() } - /// Create a new `Module` by evaluating an `AST`. + /// Get an iterator over all script-defined functions in the module. + pub fn iter_script_fn<'a>(&'a self) -> impl Iterator> + 'a { + self.functions + .values() + .map(|(_, _, _, f)| f) + .filter(|f| f.is_script()) + .map(|f| f.get_shared_fn_def()) + } + + /// Create a new `Module` by evaluating an [`AST`]. /// /// # Examples /// @@ -855,32 +960,27 @@ impl Module { /// ``` #[cfg(not(feature = "no_module"))] pub fn eval_ast_as_new(mut scope: Scope, ast: &AST, engine: &Engine) -> FuncReturn { + let mut mods = Imports::new(); + // Run the script - engine.eval_ast_with_scope_raw(&mut scope, &ast)?; + engine.eval_ast_with_scope_raw(&mut scope, &mut mods, &ast)?; // Create new module let mut module = Module::new(); - scope.into_iter().for_each( - |ScopeEntry { - typ, value, alias, .. - }| { - match typ { - // Variables with an alias left in the scope become module variables - ScopeEntryType::Normal | ScopeEntryType::Constant if alias.is_some() => { - module.variables.insert(*alias.unwrap(), value); - } - // Modules left in the scope become sub-modules - ScopeEntryType::Module if alias.is_some() => { - module - .modules - .insert(*alias.unwrap(), value.cast::()); - } - // Variables and modules with no alias are private and not exported - _ => (), + scope + .into_iter() + .for_each(|ScopeEntry { value, alias, .. }| { + // Variables with an alias left in the scope become module variables + if alias.is_some() { + module.variables.insert(*alias.unwrap(), value); } - }, - ); + }); + + // Modules left in the scope become sub-modules + mods.into_iter().for_each(|(alias, m)| { + module.modules.insert(alias.to_string(), m); + }); module.merge(ast.lib()); @@ -945,6 +1045,10 @@ impl Module { } } + if self.indexed { + return; + } + let mut variables = Vec::new(); let mut functions = Vec::new(); @@ -952,6 +1056,7 @@ impl Module { self.all_variables = variables.into_iter().collect(); self.all_functions = functions.into_iter().collect(); + self.indexed = true; } /// Does a type iterator exist in the module? @@ -962,6 +1067,7 @@ impl Module { /// Set a type iterator into the module. pub fn set_iter(&mut self, typ: TypeId, func: IteratorFn) { self.type_iterators.insert(typ, func); + self.indexed = false; } /// Get the specified type iterator. @@ -1055,6 +1161,8 @@ mod file { /// Module resolution service that loads module script files from the file system. /// + /// Script files are cached so they are are not reloaded and recompiled in subsequent requests. + /// /// The `new_with_path` and `new_with_path_and_extension` constructor functions /// allow specification of a base directory with module path used as a relative path offset /// to the base directory. The script file is then forced to be in a specified extension @@ -1073,10 +1181,16 @@ mod file { /// let mut engine = Engine::new(); /// engine.set_module_resolver(Some(resolver)); /// ``` - #[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Clone, Hash)] + #[derive(Debug)] pub struct FileModuleResolver { path: PathBuf, extension: String, + + #[cfg(not(feature = "sync"))] + cache: RefCell>, + + #[cfg(feature = "sync")] + cache: RwLock>, } impl Default for FileModuleResolver { @@ -1129,6 +1243,7 @@ mod file { Self { path: path.into(), extension: extension.into(), + cache: Default::default(), } } @@ -1173,12 +1288,45 @@ mod file { file_path.push(path); file_path.set_extension(&self.extension); // Force extension - // Compile it - let ast = engine - .compile_file(file_path) - .map_err(|err| err.new_position(pos))?; + let scope = Default::default(); - Module::eval_ast_as_new(Scope::new(), &ast, engine).map_err(|err| err.new_position(pos)) + // See if it is cached + let (module, ast) = { + #[cfg(not(feature = "sync"))] + let c = self.cache.borrow(); + #[cfg(feature = "sync")] + let c = self.cache.read().unwrap(); + + match c.get(&file_path) { + Some(ast) => ( + Module::eval_ast_as_new(scope, ast, engine) + .map_err(|err| err.new_position(pos))?, + None, + ), + None => { + // Load the file and compile it if not found + let ast = engine + .compile_file(file_path.clone()) + .map_err(|err| err.new_position(pos))?; + + ( + Module::eval_ast_as_new(scope, &ast, engine) + .map_err(|err| err.new_position(pos))?, + Some(ast), + ) + } + } + }; + + if let Some(ast) = ast { + // Put it into the cache + #[cfg(not(feature = "sync"))] + self.cache.borrow_mut().insert(file_path, ast); + #[cfg(feature = "sync")] + self.cache.write().unwrap().insert(file_path, ast); + } + + Ok(module) } } } @@ -1222,7 +1370,7 @@ mod stat { /// let mut resolver = StaticModuleResolver::new(); /// /// let module = Module::new(); - /// resolver.insert("hello".to_string(), module); + /// resolver.insert("hello", module); /// /// let mut engine = Engine::new(); /// engine.set_module_resolver(Some(resolver)); @@ -1232,17 +1380,43 @@ mod stat { } } - impl Deref for StaticModuleResolver { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.0 + impl StaticModuleResolver { + /// Add a module keyed by its path. + pub fn insert>(&mut self, path: S, mut module: Module) { + module.index_all_sub_modules(); + self.0.insert(path.into(), module); } - } - - impl DerefMut for StaticModuleResolver { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 + /// Remove a module given its path. + pub fn remove(&mut self, path: &str) -> Option { + self.0.remove(path) + } + /// Does the path exist? + pub fn contains_path(&self, path: &str) -> bool { + self.0.contains_key(path) + } + /// Get an iterator of all the modules. + pub fn iter(&self) -> impl Iterator { + self.0.iter().map(|(k, v)| (k.as_str(), v)) + } + /// Get a mutable iterator of all the modules. + pub fn iter_mut(&mut self) -> impl Iterator { + self.0.iter_mut().map(|(k, v)| (k.as_str(), v)) + } + /// Get an iterator of all the module paths. + pub fn paths(&self) -> impl Iterator { + self.0.keys().map(String::as_str) + } + /// Get an iterator of all the modules. + pub fn values(&self) -> impl Iterator { + self.0.values() + } + /// Get a mutable iterator of all the modules. + pub fn values_mut(&mut self) -> impl Iterator { + self.0.values_mut() + } + /// Remove all modules. + pub fn clear(&mut self) { + self.0.clear(); } } diff --git a/src/optimize.rs b/src/optimize.rs index 7895fc68..c7a1ed71 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -1,6 +1,6 @@ use crate::any::Dynamic; use crate::calc_fn_hash; -use crate::engine::{Engine, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_PRINT, KEYWORD_TYPE_OF}; +use crate::engine::{Engine, Imports, 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}; @@ -122,12 +122,14 @@ fn call_fn_with_constant_arguments( .engine .call_fn_raw( &mut Scope::new(), + &mut Imports::new(), &mut Default::default(), state.lib, fn_name, (hash_fn, 0), arg_values.iter_mut().collect::>().as_mut(), false, + false, None, 0, ) @@ -408,7 +410,7 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { // All other items can be thrown away. state.set_dirty(); let pos = m.1; - m.0.into_iter().find(|((name, _), _)| name == prop) + m.0.into_iter().find(|((name, _), _)| name.as_str() == prop) .map(|(_, expr)| expr.set_position(pos)) .unwrap_or_else(|| Expr::Unit(pos)) } @@ -434,7 +436,7 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { // All other items can be thrown away. state.set_dirty(); let pos = m.1; - m.0.into_iter().find(|((name, _), _)| name == s.0.as_ref()) + m.0.into_iter().find(|((name, _), _)| *name == s.0) .map(|(_, expr)| expr.set_position(pos)) .unwrap_or_else(|| Expr::Unit(pos)) } @@ -472,7 +474,7 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { // "xxx" in #{...} (Expr::StringConstant(a), Expr::Map(b)) => { state.set_dirty(); - if b.0.iter().find(|((name, _), _)| name == a.0.as_ref()).is_some() { + if b.0.iter().find(|((name, _), _)| *name == a.0).is_some() { Expr::True(a.1) } else { Expr::False(a.1) @@ -483,7 +485,7 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { state.set_dirty(); let ch = a.0.to_string(); - if b.0.iter().find(|((name, _), _)| name == &ch).is_some() { + if b.0.iter().find(|((name, _), _)| name.as_str() == ch.as_str()).is_some() { Expr::True(a.1) } else { Expr::False(a.1) @@ -628,7 +630,7 @@ fn optimize( // Add constants from the scope into the state scope - .iter() + .to_iter() .filter(|ScopeEntry { typ, expr, .. }| { // Get all the constants with definite constant expressions *typ == ScopeEntryType::Constant diff --git a/src/packages/arithmetic.rs b/src/packages/arithmetic.rs index cf109b0c..31808a2a 100644 --- a/src/packages/arithmetic.rs +++ b/src/packages/arithmetic.rs @@ -267,6 +267,19 @@ macro_rules! reg_op { $( $lib.set_fn_2($op, $func::<$par>); )* }; } +macro_rules! reg_sign { + ($lib:expr, $op:expr, $ret:ty, $($par:ty),*) => { + $( $lib.set_fn_1($op, |value: $par| -> Result<$ret, _> { + Ok(if value == (0 as $par) { + (0 as $ret) + } else if value < (0 as $par) { + (-1 as $ret) + } else { + (1 as $ret) + }) + }); )* + }; +} def_package!(crate:ArithmeticPackage:"Basic arithmetic", lib, { #[cfg(not(feature = "only_i32"))] @@ -321,6 +334,11 @@ def_package!(crate:ArithmeticPackage:"Basic arithmetic", lib, { reg_op!(lib, "%", modulo_u, i128, u128); } } + + reg_sign!(lib, "sign", INT, i8, i16, i32, i64); + + #[cfg(not(target_arch = "wasm32"))] + reg_sign!(lib, "sign", INT, i128); } // Basic arithmetic for floating-point - no need to check @@ -330,6 +348,8 @@ def_package!(crate:ArithmeticPackage:"Basic arithmetic", lib, { reg_op!(lib, "-", sub_u, f32); reg_op!(lib, "*", mul_u, f32); reg_op!(lib, "/", div_u, f32); + reg_sign!(lib, "sign", f32, f32); + reg_sign!(lib, "sign", f64, f64); } #[cfg(not(feature = "only_i32"))] diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index d492f52f..bf06ed47 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -41,12 +41,12 @@ fn pad(engine: &Engine, args: &mut [&mut Dynamic]) -> FuncRe } } - if len >= 0 { - let item = args[2].downcast_ref::().unwrap().clone(); + if len > 0 { + let item = args[2].clone(); let list = args[0].downcast_mut::().unwrap(); - while list.len() < len as usize { - push(list, item.clone())?; + if len as usize > list.len() { + list.resize(len as usize, item); } } Ok(()) diff --git a/src/packages/fn_basic.rs b/src/packages/fn_basic.rs new file mode 100644 index 00000000..01807c9e --- /dev/null +++ b/src/packages/fn_basic.rs @@ -0,0 +1,8 @@ +use crate::def_package; +use crate::fn_native::FnPtr; + +def_package!(crate:BasicFnPackage:"Basic Fn functions.", lib, { + lib.set_fn_1_mut("name", |f: &mut FnPtr| Ok(f.get_fn_name().clone())); + lib.set_getter_fn("name", |f: &mut FnPtr| Ok(f.get_fn_name().clone())); + +}); diff --git a/src/packages/map_basic.rs b/src/packages/map_basic.rs index 9be0fb42..a86e4665 100644 --- a/src/packages/map_basic.rs +++ b/src/packages/map_basic.rs @@ -6,10 +6,10 @@ use crate::engine::Map; use crate::module::FuncReturn; use crate::parser::{ImmutableString, INT}; -use crate::stdlib::{string::ToString, vec::Vec}; +use crate::stdlib::vec::Vec; fn map_get_keys(map: &mut Map) -> FuncReturn> { - Ok(map.iter().map(|(k, _)| k.to_string().into()).collect()) + Ok(map.iter().map(|(k, _)| k.clone().into()).collect()) } fn map_get_values(map: &mut Map) -> FuncReturn> { Ok(map.iter().map(|(_, v)| v.clone()).collect()) @@ -39,6 +39,17 @@ def_package!(crate:BasicMapPackage:"Basic object map utilities.", lib, { Ok(()) }, ); + lib.set_fn_2_mut( + "fill_with", + |map1: &mut Map, map2: Map| { + map2.into_iter().for_each(|(key, value)| { + if !map1.contains_key(key.as_str()) { + map1.insert(key, value); + } + }); + Ok(()) + }, + ); lib.set_fn_2_mut( "+=", |map1: &mut Map, map2: Map| { diff --git a/src/packages/mod.rs b/src/packages/mod.rs index 3b89b9f4..d75e0adc 100644 --- a/src/packages/mod.rs +++ b/src/packages/mod.rs @@ -9,6 +9,7 @@ use crate::stdlib::any::TypeId; pub(crate) mod arithmetic; mod array_basic; mod eval; +mod fn_basic; mod iter_basic; mod logic; mod map_basic; @@ -23,6 +24,8 @@ pub use arithmetic::ArithmeticPackage; #[cfg(not(feature = "no_index"))] pub use array_basic::BasicArrayPackage; pub use eval::EvalPackage; +#[cfg(not(feature = "no_function"))] +pub use fn_basic::BasicFnPackage; pub use iter_basic::BasicIteratorPackage; pub use logic::LogicPackage; #[cfg(not(feature = "no_object"))] diff --git a/src/packages/pkg_core.rs b/src/packages/pkg_core.rs index 9ceae0d9..284c5997 100644 --- a/src/packages/pkg_core.rs +++ b/src/packages/pkg_core.rs @@ -1,4 +1,5 @@ use super::arithmetic::ArithmeticPackage; +use super::fn_basic::BasicFnPackage; use super::iter_basic::BasicIteratorPackage; use super::logic::LogicPackage; use super::string_basic::BasicStringPackage; @@ -10,4 +11,5 @@ def_package!(crate:CorePackage:"_Core_ package containing basic facilities.", li LogicPackage::init(lib); BasicStringPackage::init(lib); BasicIteratorPackage::init(lib); + BasicFnPackage::init(lib); }); diff --git a/src/packages/string_basic.rs b/src/packages/string_basic.rs index 338cf5ec..2e405004 100644 --- a/src/packages/string_basic.rs +++ b/src/packages/string_basic.rs @@ -1,5 +1,6 @@ use crate::def_package; -use crate::engine::{FUNC_TO_STRING, KEYWORD_DEBUG, KEYWORD_PRINT}; +use crate::engine::{FN_TO_STRING, KEYWORD_DEBUG, KEYWORD_PRINT}; +use crate::fn_native::FnPtr; use crate::module::FuncReturn; use crate::parser::{ImmutableString, INT}; @@ -34,15 +35,16 @@ macro_rules! reg_op { } def_package!(crate:BasicStringPackage:"Basic string utilities, including printing.", lib, { - reg_op!(lib, KEYWORD_PRINT, to_string, INT, bool, char); - reg_op!(lib, FUNC_TO_STRING, to_string, INT, bool, char); + reg_op!(lib, KEYWORD_PRINT, to_string, INT, bool, char, FnPtr); + reg_op!(lib, FN_TO_STRING, to_string, INT, bool, char, FnPtr); + lib.set_fn_1_mut(KEYWORD_DEBUG, |f: &mut FnPtr| Ok(f.to_string())); lib.set_fn_0(KEYWORD_PRINT, || Ok("".to_string())); lib.set_fn_1(KEYWORD_PRINT, |_: ()| Ok("".to_string())); - lib.set_fn_1(FUNC_TO_STRING, |_: ()| Ok("".to_string())); + lib.set_fn_1(FN_TO_STRING, |_: ()| Ok("".to_string())); lib.set_fn_1(KEYWORD_PRINT, |s: ImmutableString| Ok(s)); - lib.set_fn_1(FUNC_TO_STRING, |s: ImmutableString| Ok(s)); + lib.set_fn_1(FN_TO_STRING, |s: ImmutableString| Ok(s)); reg_op!(lib, KEYWORD_DEBUG, to_debug, INT, bool, (), char, ImmutableString); @@ -50,16 +52,16 @@ def_package!(crate:BasicStringPackage:"Basic string utilities, including printin #[cfg(not(feature = "only_i64"))] { 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, FN_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); - reg_op!(lib, FUNC_TO_STRING, to_string, i64, u64); + reg_op!(lib, FN_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, FN_TO_STRING, to_string, i128, u128); reg_op!(lib, KEYWORD_DEBUG, to_debug, i128, u128); } } @@ -67,21 +69,21 @@ def_package!(crate:BasicStringPackage:"Basic string utilities, including printin #[cfg(not(feature = "no_float"))] { reg_op!(lib, KEYWORD_PRINT, to_string, f32, f64); - reg_op!(lib, FUNC_TO_STRING, to_string, f32, f64); + reg_op!(lib, FN_TO_STRING, to_string, f32, f64); reg_op!(lib, KEYWORD_DEBUG, to_debug, f32, f64); } #[cfg(not(feature = "no_index"))] { reg_op!(lib, KEYWORD_PRINT, to_debug, Array); - reg_op!(lib, FUNC_TO_STRING, to_debug, Array); + reg_op!(lib, FN_TO_STRING, to_debug, Array); reg_op!(lib, KEYWORD_DEBUG, to_debug, Array); } #[cfg(not(feature = "no_object"))] { lib.set_fn_1_mut(KEYWORD_PRINT, format_map); - lib.set_fn_1_mut(FUNC_TO_STRING, format_map); + lib.set_fn_1_mut(FN_TO_STRING, format_map); lib.set_fn_1_mut(KEYWORD_DEBUG, format_map); } diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index a472c2dd..7bc10012 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -242,24 +242,31 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str } } - let ch = *args[2].downcast_ref::< char>().unwrap(); - let s = args[0].downcast_mut::().unwrap(); + if len > 0 { + 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); + let orig_len = s.chars().count(); + + if len as usize > orig_len { + let p = s.make_mut(); + + for _ in 0..(len as usize - orig_len) { + p.push(ch); + } + + if engine.max_string_size > 0 && s.len() > engine.max_string_size { + return Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Length of string".to_string(), + engine.max_string_size, + s.len(), + Position::none(), + ))); + } + } } - 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(()) - } + Ok(()) }, ); lib.set_fn_3_mut( diff --git a/src/parser.rs b/src/parser.rs index 5d0bd902..906d99de 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,7 +2,7 @@ use crate::any::{Dynamic, Union}; use crate::calc_fn_hash; -use crate::engine::{make_getter, make_setter, Engine}; +use crate::engine::{make_getter, make_setter, Engine, KEYWORD_THIS}; use crate::error::{LexError, ParseError, ParseErrorType}; use crate::module::{Module, ModuleRef}; use crate::optimize::{optimize_into_ast, OptimizationLevel}; @@ -15,11 +15,11 @@ use crate::stdlib::{ boxed::Box, char, collections::HashMap, - format, + fmt, format, iter::empty, mem, num::NonZeroUsize, - ops::{Add, Deref, DerefMut}, + ops::Add, string::{String, ToString}, vec, vec::Vec, @@ -49,7 +49,7 @@ pub use crate::utils::ImmutableString; /// Compiled AST (abstract syntax tree) of a Rhai script. /// -/// Currently, `AST` is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`. +/// Currently, [`AST`] is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`. #[derive(Debug, Clone, Default)] pub struct AST( /// Global statements. @@ -59,36 +59,52 @@ pub struct AST( ); impl AST { - /// Create a new `AST`. + /// Create a new [`AST`]. pub fn new(statements: Vec, lib: Module) -> Self { Self(statements, lib) } /// Get the statements. + #[cfg(not(feature = "internals"))] pub(crate) fn statements(&self) -> &Vec { &self.0 } + /// Get the statements. + #[cfg(feature = "internals")] + #[deprecated(note = "this method is volatile and may change")] + pub fn statements(&self) -> &Vec { + &self.0 + } + /// Get a mutable reference to the statements. pub(crate) fn statements_mut(&mut self) -> &mut Vec { &mut self.0 } - /// Get the script-defined functions. + /// Get the internal `Module` containing all script-defined functions. + #[cfg(not(feature = "internals"))] pub(crate) fn lib(&self) -> &Module { &self.1 } - /// Merge two `AST` into one. Both `AST`'s are untouched and a new, merged, version + /// Get the internal `Module` containing all script-defined functions. + #[cfg(feature = "internals")] + #[deprecated(note = "this method is volatile and may change")] + pub fn lib(&self) -> &Module { + &self.1 + } + + /// Merge two [`AST`] into one. Both [`AST`]'s are untouched and a new, merged, version /// is returned. /// - /// The second `AST` is simply appended to the end of the first _without any processing_. - /// Thus, the return value of the first `AST` (if using expression-statement syntax) is buried. - /// Of course, if the first `AST` uses a `return` statement at the end, then - /// the second `AST` will essentially be dead code. + /// The second [`AST`] is simply appended to the end of the first _without any processing_. + /// Thus, the return value of the first [`AST`] (if using expression-statement syntax) is buried. + /// Of course, if the first [`AST`] uses a `return` statement at the end, then + /// the second [`AST`] will essentially be dead code. /// - /// All script-defined functions in the second `AST` overwrite similarly-named functions - /// in the first `AST` with the same number of parameters. + /// All script-defined functions in the second [`AST`] overwrite similarly-named functions + /// in the first [`AST`] with the same number of parameters. /// /// # Example /// @@ -100,8 +116,15 @@ impl AST { /// /// let engine = Engine::new(); /// - /// let ast1 = engine.compile(r#"fn foo(x) { 42 + x } foo(1)"#)?; - /// let ast2 = engine.compile(r#"fn foo(n) { "hello" + n } foo("!")"#)?; + /// let ast1 = engine.compile(r#" + /// fn foo(x) { 42 + x } + /// foo(1) + /// "#)?; + /// + /// let ast2 = engine.compile(r#" + /// fn foo(n) { "hello" + n } + /// foo("!") + /// "#)?; /// /// let ast = ast1.merge(&ast2); // Merge 'ast2' into 'ast1' /// @@ -122,6 +145,65 @@ impl AST { /// # } /// ``` pub fn merge(&self, other: &Self) -> Self { + self.merge_filtered(other, |_, _, _| true) + } + + /// Merge two [`AST`] into one. Both [`AST`]'s are untouched and a new, merged, version + /// is returned. + /// + /// The second [`AST`] is simply appended to the end of the first _without any processing_. + /// Thus, the return value of the first [`AST`] (if using expression-statement syntax) is buried. + /// Of course, if the first [`AST`] uses a `return` statement at the end, then + /// the second [`AST`] will essentially be dead code. + /// + /// All script-defined functions in the second [`AST`] are first selected based on a filter + /// predicate, then overwrite similarly-named functions in the first [`AST`] with the + /// same number of parameters. + /// + /// # Example + /// + /// ``` + /// # fn main() -> Result<(), Box> { + /// # #[cfg(not(feature = "no_function"))] + /// # { + /// use rhai::Engine; + /// + /// let engine = Engine::new(); + /// + /// let ast1 = engine.compile(r#" + /// fn foo(x) { 42 + x } + /// foo(1) + /// "#)?; + /// + /// let ast2 = engine.compile(r#" + /// fn foo(n) { "hello" + n } + /// fn error() { 0 } + /// foo("!") + /// "#)?; + /// + /// // Merge 'ast2', picking only 'error()' but not 'foo(_)', into 'ast1' + /// let ast = ast1.merge_filtered(&ast2, |_, name, params| name == "error" && params == 0); + /// + /// // 'ast' is essentially: + /// // + /// // fn foo(n) { 42 + n } // <- definition of 'ast1::foo' is not overwritten + /// // // because 'ast2::foo' is filtered away + /// // foo(1) // <- notice this will be 43 instead of "hello1", + /// // // but it is no longer the return value + /// // fn error() { 0 } // <- this function passes the filter and is merged + /// // foo("!") // <- returns "42!" + /// + /// // Evaluate it + /// assert_eq!(engine.eval_ast::(&ast)?, "42!"); + /// # } + /// # Ok(()) + /// # } + /// ``` + pub fn merge_filtered( + &self, + other: &Self, + filter: impl Fn(FnAccess, &str, usize) -> bool, + ) -> Self { let Self(statements, functions) = self; let ast = match (statements.is_empty(), other.0.is_empty()) { @@ -136,20 +218,47 @@ impl AST { }; let mut functions = functions.clone(); - functions.merge(&other.1); + functions.merge_filtered(&other.1, filter); Self::new(ast, functions) } - /// Clear all function definitions in the `AST`. + /// Filter out the functions, retaining only some based on a filter predicate. + /// + /// # Example + /// + /// ``` + /// # fn main() -> Result<(), Box> { + /// # #[cfg(not(feature = "no_function"))] + /// # { + /// use rhai::Engine; + /// + /// let engine = Engine::new(); + /// + /// let mut ast = engine.compile(r#" + /// fn foo(n) { n + 1 } + /// fn bar() { print("hello"); } + /// "#)?; + /// + /// // Remove all functions except 'foo(_)' + /// ast.retain_functions(|_, name, params| name == "foo" && params == 1); + /// # } + /// # Ok(()) + /// # } + /// ``` + #[cfg(not(feature = "no_function"))] + pub fn retain_functions(&mut self, filter: impl Fn(FnAccess, &str, usize) -> bool) { + self.1.retain_functions(filter); + } + + /// Clear all function definitions in the [`AST`]. #[cfg(not(feature = "no_function"))] pub fn clear_functions(&mut self) { self.1 = Default::default(); } - /// Clear all statements in the `AST`, leaving only function definitions. - #[cfg(not(feature = "no_function"))] - pub fn retain_functions(&mut self) { + /// Clear all statements in the [`AST`], leaving only function definitions. + pub fn clear_statements(&mut self) { self.0 = vec![]; } } @@ -171,6 +280,15 @@ pub enum FnAccess { Public, } +impl fmt::Display for FnAccess { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Private => write!(f, "private"), + Self::Public => write!(f, "public"), + } + } +} + /// A scripted function definition. #[derive(Debug, Clone)] pub struct ScriptFnDef { @@ -186,6 +304,25 @@ pub struct ScriptFnDef { pub pos: Position, } +impl fmt::Display for ScriptFnDef { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}{}({})", + match self.access { + FnAccess::Public => "", + FnAccess::Private => "private ", + }, + self.name, + self.params + .iter() + .map(|s| s.as_str()) + .collect::>() + .join(",") + ) + } +} + /// `return`/`throw` statement. #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] pub enum ReturnType { @@ -198,9 +335,11 @@ pub enum ReturnType { #[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)>, + pub stack: Vec<(String, ScopeEntryType)>, + /// Encapsulates a local stack with variable names to simulate an actual runtime scope. + pub modules: Vec, /// Maximum levels of expression nesting. - max_expr_depth: usize, + pub max_expr_depth: usize, /// Maximum length of a string. pub max_string_size: usize, /// Maximum length of an array. @@ -229,15 +368,12 @@ impl ParseState { /// The return value is the offset to be deducted from `Stack::len`, /// i.e. the top element of the `ParseState` is offset 1. /// Return zero when the variable name is not found in the `ParseState`. - pub fn find(&self, name: &str) -> Option { + pub fn find_var(&self, name: &str) -> Option { self.stack .iter() .rev() .enumerate() - .find(|(_, (n, typ))| match typ { - ScopeEntryType::Normal | ScopeEntryType::Constant => *n == name, - ScopeEntryType::Module => false, - }) + .find(|(_, (n, _))| *n == name) .and_then(|(i, _)| NonZeroUsize::new(i + 1)) } /// Find a module by name in the `ParseState`, searching in reverse. @@ -245,32 +381,15 @@ impl ParseState { /// i.e. the top element of the `ParseState` is offset 1. /// Return zero when the variable name is not found in the `ParseState`. pub fn find_module(&self, name: &str) -> Option { - self.stack + self.modules .iter() .rev() .enumerate() - .find(|(_, (n, typ))| match typ { - ScopeEntryType::Module => *n == name, - ScopeEntryType::Normal | ScopeEntryType::Constant => false, - }) + .find(|(_, n)| *n == name) .and_then(|(i, _)| NonZeroUsize::new(i + 1)) } } -impl Deref for ParseState { - type Target = Vec<(String, ScopeEntryType)>; - - fn deref(&self) -> &Self::Target { - &self.stack - } -} - -impl DerefMut for ParseState { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.stack - } -} - #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] /// A type that encapsulates all the settings for a particular parsing function. struct ParseSettings { @@ -464,7 +583,7 @@ pub enum Expr { /// [ expr, ... ] Array(Box<(StaticVec, Position)>), /// #{ name:expr, ... } - Map(Box<(StaticVec<((String, Position), Expr)>, Position)>), + Map(Box<(StaticVec<((ImmutableString, Position), Expr)>, Position)>), /// lhs in rhs In(Box<(Expr, Expr, Position)>), /// lhs && rhs @@ -785,22 +904,22 @@ fn parse_call_expr( mut modules: Option>, settings: ParseSettings, ) -> Result { - let (token, _) = input.peek().unwrap(); + let (token, token_pos) = input.peek().unwrap(); settings.ensure_level_within_max_limit(state.max_expr_depth)?; let mut args = StaticVec::new(); match token { - // id + // id( Token::EOF => { return Err(PERR::MissingToken( Token::RightParen.into(), format!("to close the arguments list of this function call '{}'", id), ) - .into_err(settings.pos)) + .into_err(*token_pos)) } - // id - Token::LexError(err) => return Err(err.into_err(settings.pos)), + // id( + Token::LexError(err) => return Err(err.into_err(*token_pos)), // id() Token::RightParen => { eat_token(input, Token::RightParen); @@ -1196,7 +1315,7 @@ fn parse_map_literal( } let expr = parse_expr(input, state, settings.level_up())?; - map.push(((name, pos), expr)); + map.push(((Into::::into(name), pos), expr)); } } @@ -1242,8 +1361,8 @@ fn parse_primary( state: &mut ParseState, mut settings: ParseSettings, ) -> Result { - let (token, pos1) = input.peek().unwrap(); - settings.pos = *pos1; + let (token, token_pos) = input.peek().unwrap(); + settings.pos = *token_pos; settings.ensure_level_within_max_limit(state.max_expr_depth)?; let (token, _) = match token { @@ -1263,7 +1382,7 @@ fn parse_primary( 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); + let index = state.find_var(&s); Expr::Variable(Box::new(((s, settings.pos), None, 0, index))) } Token::LeftParen => parse_paren_expr(input, state, settings.level_up())?, @@ -1453,7 +1572,7 @@ fn make_assignment_stmt<'a>( // var (indexed) = rhs Expr::Variable(x) => { let ((name, name_pos), _, _, index) = x.as_ref(); - match state.stack[(state.len() - index.unwrap().get())].1 { + match state.stack[(state.stack.len() - index.unwrap().get())].1 { ScopeEntryType::Normal => { Ok(Expr::Assignment(Box::new((lhs, fn_name.into(), rhs, pos)))) } @@ -1461,7 +1580,6 @@ fn make_assignment_stmt<'a>( ScopeEntryType::Constant => { Err(PERR::AssignmentToConstant(name.clone()).into_err(*name_pos)) } - ScopeEntryType::Module => unreachable!(), } } // xxx[???] = rhs, xxx.??? = rhs @@ -1473,7 +1591,7 @@ fn make_assignment_stmt<'a>( // 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 { + match state.stack[(state.stack.len() - index.unwrap().get())].1 { ScopeEntryType::Normal => { Ok(Expr::Assignment(Box::new((lhs, fn_name.into(), rhs, pos)))) } @@ -1481,7 +1599,6 @@ fn make_assignment_stmt<'a>( ScopeEntryType::Constant => { Err(PERR::AssignmentToConstant(name.clone()).into_err(*name_pos)) } - ScopeEntryType::Module => unreachable!(), } } // expr[???] = rhs, expr.??? = rhs @@ -1831,19 +1948,8 @@ fn parse_binary_op( #[cfg(not(feature = "no_object"))] Token::Period => { - let mut rhs = args.pop(); + let rhs = args.pop(); let current_lhs = args.pop(); - - match &mut rhs { - // current_lhs.rhs(...) - method call - Expr::FnCall(x) => { - let ((id, _, _), _, hash, args, _) = x.as_mut(); - // Recalculate function call hash because there is an additional argument - *hash = calc_fn_hash(empty(), id, args.len() + 1, empty()); - } - _ => (), - } - make_dot_expr(current_lhs, rhs, pos)? } @@ -2012,13 +2118,13 @@ fn parse_for( ensure_not_statement_expr(input, "a boolean")?; let expr = parse_expr(input, state, settings.level_up())?; - let prev_len = state.len(); - state.push((name.clone(), ScopeEntryType::Normal)); + let prev_stack_len = state.stack.len(); + state.stack.push((name.clone(), ScopeEntryType::Normal)); settings.is_breakable = true; let body = parse_block(input, state, settings.level_up())?; - state.truncate(prev_len); + state.stack.truncate(prev_stack_len); Ok(Stmt::For(Box::new((name, expr, body)))) } @@ -2041,6 +2147,16 @@ fn parse_let( (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; + // Check if the name is allowed + match name.as_str() { + KEYWORD_THIS => { + return Err( + PERR::BadInput(LexError::MalformedIdentifier(name).to_string()).into_err(pos), + ) + } + _ => (), + } + // let name = ... if match_token(input, Token::Equals)? { // let name = expr @@ -2049,34 +2165,30 @@ fn parse_let( match var_type { // let name = expr ScopeEntryType::Normal => { - state.push((name.clone(), ScopeEntryType::Normal)); + state.stack.push((name.clone(), ScopeEntryType::Normal)); Ok(Stmt::Let(Box::new(((name, pos), Some(init_value))))) } // const name = { expr:constant } ScopeEntryType::Constant if init_value.is_constant() => { - state.push((name.clone(), ScopeEntryType::Constant)); + state.stack.push((name.clone(), ScopeEntryType::Constant)); Ok(Stmt::Const(Box::new(((name, pos), init_value)))) } - // const name = expr - error + // const name = expr: error ScopeEntryType::Constant => { Err(PERR::ForbiddenConstantExpr(name).into_err(init_value.position())) } - // Variable cannot be a module - ScopeEntryType::Module => unreachable!(), } } else { // let name match var_type { ScopeEntryType::Normal => { - state.push((name.clone(), ScopeEntryType::Normal)); + state.stack.push((name.clone(), ScopeEntryType::Normal)); Ok(Stmt::Let(Box::new(((name, pos), None)))) } ScopeEntryType::Constant => { - state.push((name.clone(), ScopeEntryType::Constant)); + state.stack.push((name.clone(), ScopeEntryType::Constant)); Ok(Stmt::Const(Box::new(((name, pos), Expr::Unit(pos))))) } - // Variable cannot be a module - ScopeEntryType::Module => unreachable!(), } } } @@ -2112,7 +2224,7 @@ fn parse_import( (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; - state.push((name.clone(), ScopeEntryType::Module)); + state.modules.push(name.clone()); Ok(Stmt::Import(Box::new((expr, (name, settings.pos))))) } @@ -2199,7 +2311,8 @@ fn parse_block( settings.ensure_level_within_max_limit(state.max_expr_depth)?; let mut statements = StaticVec::new(); - let prev_len = state.len(); + let prev_stack_len = state.stack.len(); + let prev_mods_len = state.modules.len(); while !match_token(input, Token::RightBrace)? { // Parse statements inside the block @@ -2239,7 +2352,8 @@ fn parse_block( } } - state.truncate(prev_len); + state.stack.truncate(prev_stack_len); + state.modules.truncate(prev_mods_len); Ok(Stmt::Block(Box::new((statements, settings.pos)))) } @@ -2374,7 +2488,7 @@ fn parse_fn( (Token::RightParen, _) => (), _ => match input.next().unwrap() { (Token::Identifier(s), pos) => { - state.push((s.clone(), ScopeEntryType::Normal)); + state.stack.push((s.clone(), ScopeEntryType::Normal)); params.push((s, pos)) } (Token::LexError(err), pos) => return Err(err.into_err(pos)), diff --git a/src/result.rs b/src/result.rs index 760a9a5e..06bc80d3 100644 --- a/src/result.rs +++ b/src/result.rs @@ -22,6 +22,7 @@ use crate::stdlib::path::PathBuf; /// /// Currently, `EvalAltResult` is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`. #[derive(Debug)] +#[non_exhaustive] pub enum EvalAltResult { /// Syntax error. ErrorParsing(ParseErrorType, Position), @@ -38,6 +39,8 @@ pub enum EvalAltResult { /// An error has occurred inside a called function. /// Wrapped values re the name of the function and the interior error. ErrorInFunctionCall(String, Box, Position), + /// Access to `this` that is not bounded. + ErrorUnboundedThis(Position), /// Non-boolean operand encountered for boolean operator. Wrapped value is the operator. ErrorBooleanArgMismatch(String, Position), /// Non-character value encountered where a character is required. @@ -109,6 +112,7 @@ impl EvalAltResult { Self::ErrorParsing(p, _) => p.desc(), Self::ErrorInFunctionCall(_, _, _) => "Error in called function", Self::ErrorFunctionNotFound(_, _) => "Function not found", + Self::ErrorUnboundedThis(_) => "'this' is not bounded", Self::ErrorBooleanArgMismatch(_, _) => "Boolean operator expects boolean operands", Self::ErrorCharMismatch(_) => "Character expected", Self::ErrorNumericIndexExpr(_) => { @@ -183,6 +187,7 @@ impl fmt::Display for EvalAltResult { Self::ErrorIndexingType(_, _) | Self::ErrorNumericIndexExpr(_) | Self::ErrorStringIndexExpr(_) + | Self::ErrorUnboundedThis(_) | Self::ErrorImportExpr(_) | Self::ErrorLogicGuard(_) | Self::ErrorFor(_) @@ -269,6 +274,7 @@ impl EvalAltResult { Self::ErrorParsing(_, pos) | Self::ErrorFunctionNotFound(_, pos) | Self::ErrorInFunctionCall(_, _, pos) + | Self::ErrorUnboundedThis(pos) | Self::ErrorBooleanArgMismatch(_, pos) | Self::ErrorCharMismatch(pos) | Self::ErrorArrayBounds(_, _, pos) @@ -308,6 +314,7 @@ impl EvalAltResult { Self::ErrorParsing(_, pos) | Self::ErrorFunctionNotFound(_, pos) | Self::ErrorInFunctionCall(_, _, pos) + | Self::ErrorUnboundedThis(pos) | Self::ErrorBooleanArgMismatch(_, pos) | Self::ErrorCharMismatch(pos) | Self::ErrorArrayBounds(_, _, pos) diff --git a/src/scope.rs b/src/scope.rs index 97d3770e..56b3a8cc 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,7 +1,6 @@ //! Module that defines the `Scope` type representing a function call-stack scope. -use crate::any::{Dynamic, Union, Variant}; -use crate::module::Module; +use crate::any::{Dynamic, Variant}; use crate::parser::{map_dynamic_to_expr, Expr}; use crate::token::Position; @@ -14,9 +13,6 @@ pub enum EntryType { Normal, /// Immutable constant value. Constant, - /// Name of a module, allowing member access with the :: operator. - /// This is for internal use only. - Module, } /// An entry in the Scope. @@ -171,32 +167,6 @@ impl<'a> Scope<'a> { self.push_dynamic_value(name, EntryType::Normal, value, false); } - /// Add (push) a new module to the Scope. - /// - /// 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, 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( - name, - EntryType::Module, - Dynamic(Union::Module(Box::new(value))), - false, - ); - } - /// Add (push) a new constant to the Scope. /// /// Constants are immutable and cannot be assigned to. Their values never change. @@ -312,66 +282,26 @@ impl<'a> Scope<'a> { self.0 .iter() .rev() // Always search a Scope in reverse order - .any(|Entry { name: key, typ, .. }| match typ { - EntryType::Normal | EntryType::Constant => name == key, - EntryType::Module => false, - }) + .any(|Entry { name: key, .. }| name == key) } /// Find an entry in the Scope, starting from the last. - /// - /// modules are ignored. pub(crate) fn get_index(&self, name: &str) -> Option<(usize, EntryType)> { self.0 .iter() .enumerate() .rev() // Always search a Scope in reverse order - .find_map(|(index, Entry { name: key, typ, .. })| match typ { - EntryType::Normal | EntryType::Constant => { - if name == key { - Some((index, *typ)) - } else { - None - } + .find_map(|(index, Entry { name: key, typ, .. })| { + if name == key { + Some((index, *typ)) + } else { + None } - EntryType::Module => None, }) } - /// Find a module in the Scope, starting from the last. - pub(crate) fn get_module_index(&self, name: &str) -> Option { - self.0 - .iter() - .enumerate() - .rev() // Always search a Scope in reverse order - .find_map(|(index, Entry { name: key, typ, .. })| match typ { - EntryType::Module => { - if name == key { - Some(index) - } else { - None - } - } - EntryType::Normal | EntryType::Constant => None, - }) - } - - /// 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::() - } - /// Get the value of an entry in the Scope, starting from the last. /// - /// modules are ignored. - /// /// # Examples /// /// ``` @@ -386,10 +316,7 @@ impl<'a> Scope<'a> { self.0 .iter() .rev() - .find(|Entry { name: key, typ, .. }| match typ { - EntryType::Normal | EntryType::Constant => name == key, - EntryType::Module => false, - }) + .find(|Entry { name: key, .. }| name == key) .and_then(|Entry { value, .. }| value.downcast_ref::().cloned()) } @@ -417,12 +344,10 @@ 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)) => unreachable!("variable {} is constant", name), + Some((_, EntryType::Constant)) => panic!("variable {} is constant", name), Some((index, EntryType::Normal)) => { self.0.get_mut(index).unwrap().value = Dynamic::from(value) } - // modules cannot be modified - Some((_, EntryType::Module)) => unreachable!(), } } @@ -444,9 +369,37 @@ impl<'a> Scope<'a> { } /// Get an iterator to entries in the Scope. - pub(crate) fn iter(&self) -> impl Iterator { + pub(crate) fn to_iter(&self) -> impl Iterator { self.0.iter().rev() // Always search a Scope in reverse order } + + /// Get an iterator to entries in the Scope. + /// + /// # Examples + /// + /// ``` + /// use rhai::{Dynamic, Scope}; + /// + /// let mut my_scope = Scope::new(); + /// + /// my_scope.push("x", 42_i64); + /// my_scope.push("foo", "hello".to_string()); + /// + /// let mut iter = my_scope.iter(); + /// + /// let (name, value) = iter.next().unwrap(); + /// assert_eq!(name, "x"); + /// assert_eq!(value.clone().cast::(), 42); + /// + /// let (name, value) = iter.next().unwrap(); + /// assert_eq!(name, "foo"); + /// assert_eq!(value.clone().cast::(), "hello"); + /// ``` + pub fn iter(&self) -> impl Iterator { + self.0 + .iter() + .map(|Entry { name, value, .. }| (name.as_ref(), value)) + } } impl<'a, K: Into>> iter::Extend<(K, EntryType, Dynamic)> for Scope<'a> { diff --git a/src/token.rs b/src/token.rs index d5f84902..4b19ab49 100644 --- a/src/token.rs +++ b/src/token.rs @@ -202,11 +202,14 @@ pub enum Token { PowerOfAssign, #[cfg(not(feature = "no_function"))] Private, + #[cfg(not(feature = "no_module"))] Import, #[cfg(not(feature = "no_module"))] Export, + #[cfg(not(feature = "no_module"))] As, LexError(Box), + Comment(String), EOF, } @@ -427,24 +430,715 @@ impl From for String { } } -/// An iterator on a `Token` stream. -pub struct TokenIterator<'a> { +/// State of the tokenizer. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)] +pub struct TokenizeState { /// Maximum length of a string (0 = unlimited). - max_string_size: usize, + pub max_string_size: usize, /// Can the next token be a unary operator? - can_be_unary: bool, - /// Current position. - pos: Position, + pub non_unary: bool, + /// Is the tokenizer currently inside a block comment? + pub comment_level: usize, + /// Return `None` at the end of the stream instead of `Some(Token::EOF)`? + pub end_with_none: bool, + /// Include comments? + pub include_comments: bool, +} + +/// Trait that encapsulates a peekable character input stream. +pub trait InputStream { + /// Get the next character + fn get_next(&mut self) -> Option; + /// Peek the next character + fn peek_next(&mut self) -> Option; +} + +pub fn is_valid_identifier(name: impl Iterator) -> bool { + let mut first_alphabetic = false; + + for ch in name { + match ch { + '_' => (), + _ if char::is_ascii_alphabetic(&ch) => first_alphabetic = true, + _ if !first_alphabetic => return false, + _ if char::is_ascii_alphanumeric(&ch) => (), + _ => return false, + } + } + + first_alphabetic +} + +/// Parse a string literal wrapped by `enclosing_char`. +pub fn parse_string_literal( + stream: &mut impl InputStream, + state: &mut TokenizeState, + pos: &mut Position, + enclosing_char: char, +) -> Result { + let mut result = Vec::new(); + let mut escape = String::with_capacity(12); + + loop { + let next_char = stream.get_next().ok_or((LERR::UnterminatedString, *pos))?; + + pos.advance(); + + if state.max_string_size > 0 && result.len() > state.max_string_size { + return Err((LexError::StringTooLong(state.max_string_size), *pos)); + } + + match next_char { + // \... + '\\' if escape.is_empty() => { + escape.push('\\'); + } + // \\ + '\\' if !escape.is_empty() => { + escape.clear(); + result.push('\\'); + } + // \t + 't' if !escape.is_empty() => { + escape.clear(); + result.push('\t'); + } + // \n + 'n' if !escape.is_empty() => { + escape.clear(); + result.push('\n'); + } + // \r + 'r' if !escape.is_empty() => { + escape.clear(); + result.push('\r'); + } + // \x??, \u????, \U???????? + ch @ 'x' | ch @ 'u' | ch @ 'U' if !escape.is_empty() => { + let mut seq = escape.clone(); + seq.push(ch); + escape.clear(); + + let mut out_val: u32 = 0; + let len = match ch { + 'x' => 2, + 'u' => 4, + 'U' => 8, + _ => unreachable!(), + }; + + for _ in 0..len { + let c = stream + .get_next() + .ok_or_else(|| (LERR::MalformedEscapeSequence(seq.to_string()), *pos))?; + + seq.push(c); + pos.advance(); + + out_val *= 16; + out_val += c + .to_digit(16) + .ok_or_else(|| (LERR::MalformedEscapeSequence(seq.to_string()), *pos))?; + } + + result.push( + char::from_u32(out_val) + .ok_or_else(|| (LERR::MalformedEscapeSequence(seq), *pos))?, + ); + } + + // \{enclosing_char} - escaped + ch if enclosing_char == ch && !escape.is_empty() => { + escape.clear(); + result.push(ch) + } + + // Close wrapper + ch if enclosing_char == ch && escape.is_empty() => break, + + // Unknown escape sequence + _ if !escape.is_empty() => return Err((LERR::MalformedEscapeSequence(escape), *pos)), + + // Cannot have new-lines inside string literals + '\n' => { + pos.rewind(); + return Err((LERR::UnterminatedString, *pos)); + } + + // All other characters + ch => { + escape.clear(); + result.push(ch); + } + } + } + + let s = result.iter().collect::(); + + if state.max_string_size > 0 && s.len() > state.max_string_size { + return Err((LexError::StringTooLong(state.max_string_size), *pos)); + } + + Ok(s) +} + +/// Consume the next character. +fn eat_next(stream: &mut impl InputStream, pos: &mut Position) { + stream.get_next(); + pos.advance(); +} + +/// Scan for a block comment until the end. +fn scan_comment( + stream: &mut impl InputStream, + state: &mut TokenizeState, + pos: &mut Position, + comment: &mut String, +) { + while let Some(c) = stream.get_next() { + pos.advance(); + + if state.include_comments { + comment.push(c); + } + + match c { + '/' => { + if let Some(c2) = stream.get_next() { + if state.include_comments { + comment.push(c2); + } + if c2 == '*' { + state.comment_level += 1; + } + } + pos.advance(); + } + '*' => { + if let Some(c2) = stream.get_next() { + if state.include_comments { + comment.push(c2); + } + if c2 == '/' { + state.comment_level -= 1; + } + } + pos.advance(); + } + '\n' => pos.new_line(), + _ => (), + } + + if state.comment_level == 0 { + break; + } + } +} + +/// Get the next token. +pub fn get_next_token( + stream: &mut impl InputStream, + state: &mut TokenizeState, + pos: &mut Position, +) -> Option<(Token, Position)> { + let result = get_next_token_inner(stream, state, pos); + + // Save the last token's state + if let Some((token, _)) = &result { + state.non_unary = !token.is_next_unary(); + } + + result +} + +/// Get the next token. +fn get_next_token_inner( + stream: &mut impl InputStream, + state: &mut TokenizeState, + pos: &mut Position, +) -> Option<(Token, Position)> { + // Still inside a comment? + if state.comment_level > 0 { + let start_pos = *pos; + let mut comment = String::new(); + scan_comment(stream, state, pos, &mut comment); + + if state.include_comments { + return Some((Token::Comment(comment), start_pos)); + } + } + + let mut negated = false; + + while let Some(c) = stream.get_next() { + pos.advance(); + + let start_pos = *pos; + + match (c, stream.peek_next().unwrap_or('\0')) { + // \n + ('\n', _) => pos.new_line(), + + // digit ... + ('0'..='9', _) => { + let mut result = Vec::new(); + let mut radix_base: Option = None; + result.push(c); + + while let Some(next_char) = stream.peek_next() { + match next_char { + '0'..='9' | '_' => { + result.push(next_char); + eat_next(stream, pos); + } + #[cfg(not(feature = "no_float"))] + '.' => { + result.push(next_char); + eat_next(stream, pos); + while let Some(next_char_in_float) = stream.peek_next() { + match next_char_in_float { + '0'..='9' | '_' => { + result.push(next_char_in_float); + eat_next(stream, pos); + } + _ => break, + } + } + } + // 0x????, 0o????, 0b???? + ch @ 'x' | ch @ 'X' | ch @ 'o' | ch @ 'O' | ch @ 'b' | ch @ 'B' if c == '0' => { + result.push(next_char); + eat_next(stream, pos); + + let valid = match ch { + 'x' | 'X' => [ + 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_', + ], + 'o' | 'O' => [ + '0', '1', '2', '3', '4', '5', '6', '7', '_', '_', '_', '_', + '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', + ], + 'b' | 'B' => [ + '0', '1', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', + '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', + ], + _ => unreachable!(), + }; + + radix_base = Some(match ch { + 'x' | 'X' => 16, + 'o' | 'O' => 8, + 'b' | 'B' => 2, + _ => unreachable!(), + }); + + while let Some(next_char_in_escape_seq) = stream.peek_next() { + if !valid.contains(&next_char_in_escape_seq) { + break; + } + + result.push(next_char_in_escape_seq); + eat_next(stream, pos); + } + } + + _ => break, + } + } + + if negated { + result.insert(0, '-'); + } + + // Parse number + if let Some(radix) = radix_base { + let out: String = result.iter().skip(2).filter(|&&c| c != '_').collect(); + + return Some(( + INT::from_str_radix(&out, radix) + .map(Token::IntegerConstant) + .unwrap_or_else(|_| { + Token::LexError(Box::new(LERR::MalformedNumber( + result.into_iter().collect(), + ))) + }), + start_pos, + )); + } else { + let out: String = result.iter().filter(|&&c| c != '_').collect(); + let num = INT::from_str(&out).map(Token::IntegerConstant); + + // If integer parsing is unnecessary, try float instead + #[cfg(not(feature = "no_float"))] + let num = num.or_else(|_| FLOAT::from_str(&out).map(Token::FloatConstant)); + + return Some(( + num.unwrap_or_else(|_| { + Token::LexError(Box::new(LERR::MalformedNumber( + result.into_iter().collect(), + ))) + }), + start_pos, + )); + } + } + + // letter or underscore ... + ('A'..='Z', _) | ('a'..='z', _) | ('_', _) => { + let mut result = Vec::new(); + result.push(c); + + while let Some(next_char) = stream.peek_next() { + match next_char { + x if x.is_ascii_alphanumeric() || x == '_' => { + result.push(x); + eat_next(stream, pos); + } + _ => break, + } + } + + let is_valid_identifier = is_valid_identifier(result.iter().cloned()); + + let identifier: String = result.into_iter().collect(); + + if !is_valid_identifier { + return Some(( + Token::LexError(Box::new(LERR::MalformedIdentifier(identifier))), + start_pos, + )); + } + + return Some(( + match identifier.as_str() { + "true" => Token::True, + "false" => Token::False, + "let" => Token::Let, + "const" => Token::Const, + "if" => Token::If, + "else" => Token::Else, + "while" => Token::While, + "loop" => Token::Loop, + "continue" => Token::Continue, + "break" => Token::Break, + "return" => Token::Return, + "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"))] + "fn" => Token::Fn, + + _ => Token::Identifier(identifier), + }, + start_pos, + )); + } + + // " - string literal + ('"', _) => return parse_string_literal(stream, state, pos, '"') + .map_or_else( + |err| Some((Token::LexError(Box::new(err.0)), err.1)), + |out| Some((Token::StringConst(out), start_pos)), + ), + + // ' - character literal + ('\'', '\'') => return Some(( + Token::LexError(Box::new(LERR::MalformedChar("".to_string()))), + start_pos, + )), + ('\'', _) => return Some( + parse_string_literal(stream, state, pos, '\'') + .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))), + start_pos, + ) + } else { + (Token::CharConstant(first.expect("should be Some")), start_pos) + } + }, + ), + ), + + // Braces + ('{', _) => return Some((Token::LeftBrace, start_pos)), + ('}', _) => return Some((Token::RightBrace, start_pos)), + + // Parentheses + ('(', _) => return Some((Token::LeftParen, start_pos)), + (')', _) => return Some((Token::RightParen, start_pos)), + + // Indexing + ('[', _) => return Some((Token::LeftBracket, start_pos)), + (']', _) => return Some((Token::RightBracket, start_pos)), + + // Map literal + #[cfg(not(feature = "no_object"))] + ('#', '{') => { + eat_next(stream, pos); + return Some((Token::MapStart, start_pos)); + } + + // Operators + ('+', '=') => { + eat_next(stream, pos); + return Some((Token::PlusAssign, start_pos)); + } + ('+', _) if !state.non_unary => return Some((Token::UnaryPlus, start_pos)), + ('+', _) => return Some((Token::Plus, start_pos)), + + ('-', '0'..='9') if !state.non_unary => negated = true, + ('-', '0'..='9') => return Some((Token::Minus, start_pos)), + ('-', '=') => { + eat_next(stream, pos); + return Some((Token::MinusAssign, start_pos)); + } + ('-', '>') => return Some(( + Token::LexError(Box::new(LERR::ImproperSymbol( + "'->' is not a valid symbol. This is not C or C++!".to_string(), + ))), + start_pos, + )), + ('-', _) if !state.non_unary => return Some((Token::UnaryMinus, start_pos)), + ('-', _) => return Some((Token::Minus, start_pos)), + + ('*', '=') => { + eat_next(stream, pos); + return Some((Token::MultiplyAssign, start_pos)); + } + ('*', _) => return Some((Token::Multiply, start_pos)), + + // Comments + ('/', '/') => { + eat_next(stream, pos); + + let mut comment = if state.include_comments { + "//".to_string() + } else { + String::new() + }; + + while let Some(c) = stream.get_next() { + if c == '\n' { + pos.new_line(); + break; + } + + if state.include_comments { + comment.push(c); + } + pos.advance(); + } + + if state.include_comments { + return Some((Token::Comment(comment), start_pos)); + } + } + ('/', '*') => { + state.comment_level = 1; + + eat_next(stream, pos); + + let mut comment = if state.include_comments { + "/*".to_string() + } else { + String::new() + }; + scan_comment(stream, state, pos, &mut comment); + + if state.include_comments { + return Some((Token::Comment(comment), start_pos)); + } + } + + ('/', '=') => { + eat_next(stream, pos); + return Some((Token::DivideAssign, start_pos)); + } + ('/', _) => return Some((Token::Divide, start_pos)), + + (';', _) => return Some((Token::SemiColon, start_pos)), + (',', _) => return Some((Token::Comma, start_pos)), + ('.', _) => return Some((Token::Period, start_pos)), + + ('=', '=') => { + eat_next(stream, pos); + + // Warn against `===` + if stream.peek_next() == Some('=') { + return Some(( + Token::LexError(Box::new(LERR::ImproperSymbol( + "'===' is not a valid operator. This is not JavaScript! Should it be '=='?" + .to_string(), + ))), + start_pos, + )); + } + + return Some((Token::EqualsTo, start_pos)); + } + ('=', '>') => return Some(( + Token::LexError(Box::new(LERR::ImproperSymbol( + "'=>' is not a valid symbol. This is not Rust! Should it be '>='?" + .to_string(), + ))), + start_pos, + )), + ('=', _) => return Some((Token::Equals, start_pos)), + + (':', ':') => { + eat_next(stream, pos); + return Some((Token::DoubleColon, start_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(), + ))), + start_pos, + )), + (':', _) => return Some((Token::Colon, start_pos)), + + ('<', '=') => { + eat_next(stream, pos); + return Some((Token::LessThanEqualsTo, start_pos)); + } + ('<', '-') => return Some(( + Token::LexError(Box::new(LERR::ImproperSymbol( + "'<-' is not a valid symbol. Should it be '<='?".to_string(), + ))), + start_pos, + )), + ('<', '<') => { + eat_next(stream, pos); + + return Some(( + if stream.peek_next() == Some('=') { + eat_next(stream, pos); + Token::LeftShiftAssign + } else { + Token::LeftShift + }, + start_pos, + )); + } + ('<', _) => return Some((Token::LessThan, start_pos)), + + ('>', '=') => { + eat_next(stream, pos); + return Some((Token::GreaterThanEqualsTo, start_pos)); + } + ('>', '>') => { + eat_next(stream, pos); + + return Some(( + if stream.peek_next() == Some('=') { + eat_next(stream, pos); + Token::RightShiftAssign + } else { + Token::RightShift + }, + start_pos, + )); + } + ('>', _) => return Some((Token::GreaterThan, start_pos)), + + ('!', '=') => { + eat_next(stream, pos); + + // Warn against `!==` + if stream.peek_next() == Some('=') { + return Some(( + Token::LexError(Box::new(LERR::ImproperSymbol( + "'!==' is not a valid operator. This is not JavaScript! Should it be '!='?" + .to_string(), + ))), + start_pos, + )); + } + + return Some((Token::NotEqualsTo, start_pos)); + } + ('!', _) => return Some((Token::Bang, start_pos)), + + ('|', '|') => { + eat_next(stream, pos); + return Some((Token::Or, start_pos)); + } + ('|', '=') => { + eat_next(stream, pos); + return Some((Token::OrAssign, start_pos)); + } + ('|', _) => return Some((Token::Pipe, start_pos)), + + ('&', '&') => { + eat_next(stream, pos); + return Some((Token::And, start_pos)); + } + ('&', '=') => { + eat_next(stream, pos); + return Some((Token::AndAssign, start_pos)); + } + ('&', _) => return Some((Token::Ampersand, start_pos)), + + ('^', '=') => { + eat_next(stream, pos); + return Some((Token::XOrAssign, start_pos)); + } + ('^', _) => return Some((Token::XOr, start_pos)), + + ('%', '=') => { + eat_next(stream, pos); + return Some((Token::ModuloAssign, start_pos)); + } + ('%', _) => return Some((Token::Modulo, start_pos)), + + ('~', '=') => { + eat_next(stream, pos); + return Some((Token::PowerOfAssign, start_pos)); + } + ('~', _) => return Some((Token::PowerOf, start_pos)), + + ('\0', _) => unreachable!(), + + (ch, _) if ch.is_whitespace() => (), + (ch, _) => return Some((Token::LexError(Box::new(LERR::UnexpectedChar(ch))), start_pos)), + } + } + + pos.advance(); + + if state.end_with_none { + None + } else { + Some((Token::EOF, *pos)) + } +} + +/// A type that implements the `InputStream` trait. +/// Multiple charaacter streams are jointed together to form one single stream. +pub struct MultiInputsStream<'a> { /// The input character streams. streams: StaticVec>>, } -impl<'a> TokenIterator<'a> { - /// Consume the next character. - fn eat_next(&mut self) { - self.get_next(); - self.advance(); - } +impl InputStream for MultiInputsStream<'_> { /// Get the next character fn get_next(&mut self) -> Option { loop { @@ -475,638 +1169,39 @@ impl<'a> TokenIterator<'a> { } } } - /// Move the current position one character ahead. - fn advance(&mut self) { - self.pos.advance(); - } - /// Move the current position back one character. - /// - /// # Panics - /// - /// Panics if already at the beginning of a line - cannot rewind to the previous line. - fn rewind(&mut self) { - self.pos.rewind(); - } - /// Move the current position to the next line. - fn new_line(&mut self) { - self.pos.new_line() - } - - /// Parse a string literal wrapped by `enclosing_char`. - 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); - - loop { - let next_char = self - .get_next() - .ok_or((LERR::UnterminatedString, self.pos))?; - - 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() => { - escape.push('\\'); - } - // \\ - '\\' if !escape.is_empty() => { - escape.clear(); - result.push('\\'); - } - // \t - 't' if !escape.is_empty() => { - escape.clear(); - result.push('\t'); - } - // \n - 'n' if !escape.is_empty() => { - escape.clear(); - result.push('\n'); - } - // \r - 'r' if !escape.is_empty() => { - escape.clear(); - result.push('\r'); - } - // \x??, \u????, \U???????? - ch @ 'x' | ch @ 'u' | ch @ 'U' if !escape.is_empty() => { - let mut seq = escape.clone(); - seq.push(ch); - escape.clear(); - - let mut out_val: u32 = 0; - let len = match ch { - 'x' => 2, - 'u' => 4, - 'U' => 8, - _ => unreachable!(), - }; - - for _ in 0..len { - let c = self.get_next().ok_or_else(|| { - (LERR::MalformedEscapeSequence(seq.to_string()), self.pos) - })?; - - seq.push(c); - self.advance(); - - out_val *= 16; - out_val += c.to_digit(16).ok_or_else(|| { - (LERR::MalformedEscapeSequence(seq.to_string()), self.pos) - })?; - } - - result.push( - char::from_u32(out_val) - .ok_or_else(|| (LERR::MalformedEscapeSequence(seq), self.pos))?, - ); - } - - // \{enclosing_char} - escaped - ch if enclosing_char == ch && !escape.is_empty() => { - escape.clear(); - result.push(ch) - } - - // Close wrapper - ch if enclosing_char == ch && escape.is_empty() => break, - - // Unknown escape sequence - _ if !escape.is_empty() => { - return Err((LERR::MalformedEscapeSequence(escape), self.pos)) - } - - // Cannot have new-lines inside string literals - '\n' => { - self.rewind(); - return Err((LERR::UnterminatedString, self.pos)); - } - - // All other characters - ch => { - escape.clear(); - result.push(ch); - } - } - } - - 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. - fn inner_next(&mut self) -> Option<(Token, Position)> { - let mut negated = false; - - while let Some(c) = self.get_next() { - self.advance(); - - let pos = self.pos; - - match (c, self.peek_next().unwrap_or('\0')) { - // \n - ('\n', _) => self.new_line(), - - // digit ... - ('0'..='9', _) => { - let mut result = Vec::new(); - let mut radix_base: Option = None; - result.push(c); - - while let Some(next_char) = self.peek_next() { - match next_char { - '0'..='9' | '_' => { - result.push(next_char); - self.eat_next(); - } - #[cfg(not(feature = "no_float"))] - '.' => { - result.push(next_char); - self.eat_next(); - while let Some(next_char_in_float) = self.peek_next() { - match next_char_in_float { - '0'..='9' | '_' => { - result.push(next_char_in_float); - self.eat_next(); - } - _ => break, - } - } - } - // 0x????, 0o????, 0b???? - ch @ 'x' | ch @ 'X' | ch @ 'o' | ch @ 'O' | ch @ 'b' | ch @ 'B' - if c == '0' => - { - result.push(next_char); - self.eat_next(); - - let valid = match ch { - 'x' | 'X' => [ - 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_', - ], - 'o' | 'O' => [ - '0', '1', '2', '3', '4', '5', '6', '7', '_', '_', '_', '_', - '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', - ], - 'b' | 'B' => [ - '0', '1', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', - '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', - ], - _ => unreachable!(), - }; - - radix_base = Some(match ch { - 'x' | 'X' => 16, - 'o' | 'O' => 8, - 'b' | 'B' => 2, - _ => unreachable!(), - }); - - while let Some(next_char_in_hex) = self.peek_next() { - if !valid.contains(&next_char_in_hex) { - break; - } - - result.push(next_char_in_hex); - self.eat_next(); - } - } - - _ => break, - } - } - - if negated { - result.insert(0, '-'); - } - - // Parse number - if let Some(radix) = radix_base { - let out: String = result.iter().skip(2).filter(|&&c| c != '_').collect(); - - return Some(( - INT::from_str_radix(&out, radix) - .map(Token::IntegerConstant) - .unwrap_or_else(|_| { - Token::LexError(Box::new(LERR::MalformedNumber( - result.into_iter().collect(), - ))) - }), - pos, - )); - } else { - let out: String = result.iter().filter(|&&c| c != '_').collect(); - let num = INT::from_str(&out).map(Token::IntegerConstant); - - // If integer parsing is unnecessary, try float instead - #[cfg(not(feature = "no_float"))] - let num = num.or_else(|_| FLOAT::from_str(&out).map(Token::FloatConstant)); - - return Some(( - num.unwrap_or_else(|_| { - Token::LexError(Box::new(LERR::MalformedNumber( - result.into_iter().collect(), - ))) - }), - pos, - )); - } - } - - // letter or underscore ... - ('A'..='Z', _) | ('a'..='z', _) | ('_', _) => { - let mut result = Vec::new(); - result.push(c); - - while let Some(next_char) = self.peek_next() { - match next_char { - x if x.is_ascii_alphanumeric() || x == '_' => { - result.push(x); - self.eat_next(); - } - _ => break, - } - } - - let is_valid_identifier = result - .iter() - .find(|&ch| char::is_ascii_alphanumeric(ch)) // first alpha-numeric character - .map(char::is_ascii_alphabetic) // is a letter - .unwrap_or(false); // if no alpha-numeric at all - syntax error - - let identifier: String = result.iter().collect(); - - if !is_valid_identifier { - return Some(( - Token::LexError(Box::new(LERR::MalformedIdentifier(identifier))), - pos, - )); - } - - return Some(( - match identifier.as_str() { - "true" => Token::True, - "false" => Token::False, - "let" => Token::Let, - "const" => Token::Const, - "if" => Token::If, - "else" => Token::Else, - "while" => Token::While, - "loop" => Token::Loop, - "continue" => Token::Continue, - "break" => Token::Break, - "return" => Token::Return, - "throw" => Token::Throw, - "for" => Token::For, - "in" => Token::In, - #[cfg(not(feature = "no_function"))] - "private" => Token::Private, - "import" => Token::Import, - #[cfg(not(feature = "no_module"))] - "export" => Token::Export, - "as" => Token::As, - - #[cfg(not(feature = "no_function"))] - "fn" => Token::Fn, - - _ => Token::Identifier(identifier), - }, - pos, - )); - } - - // " - string literal - ('"', _) => { - 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 - ('\'', '\'') => { - return Some(( - Token::LexError(Box::new(LERR::MalformedChar("".to_string()))), - pos, - )); - } - ('\'', _) => { - 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) - } - }, - ), - ); - } - - // Braces - ('{', _) => return Some((Token::LeftBrace, pos)), - ('}', _) => return Some((Token::RightBrace, pos)), - - // Parentheses - ('(', _) => return Some((Token::LeftParen, pos)), - (')', _) => return Some((Token::RightParen, pos)), - - // Indexing - ('[', _) => return Some((Token::LeftBracket, pos)), - (']', _) => return Some((Token::RightBracket, pos)), - - // Map literal - #[cfg(not(feature = "no_object"))] - ('#', '{') => { - self.eat_next(); - return Some((Token::MapStart, pos)); - } - - // Operators - ('+', '=') => { - self.eat_next(); - return Some((Token::PlusAssign, pos)); - } - ('+', _) if self.can_be_unary => return Some((Token::UnaryPlus, pos)), - ('+', _) => return Some((Token::Plus, pos)), - - ('-', '0'..='9') if self.can_be_unary => negated = true, - ('-', '0'..='9') => return Some((Token::Minus, pos)), - ('-', '=') => { - 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)), - - ('*', '=') => { - self.eat_next(); - return Some((Token::MultiplyAssign, pos)); - } - ('*', _) => return Some((Token::Multiply, pos)), - - // Comments - ('/', '/') => { - self.eat_next(); - - while let Some(c) = self.get_next() { - if c == '\n' { - self.new_line(); - break; - } - - self.advance(); - } - } - ('/', '*') => { - let mut level = 1; - - self.eat_next(); - - while let Some(c) = self.get_next() { - self.advance(); - - match c { - '/' => { - if self.get_next() == Some('*') { - level += 1; - } - self.advance(); - } - '*' => { - if self.get_next() == Some('/') { - level -= 1; - } - self.advance(); - } - '\n' => self.new_line(), - _ => (), - } - - if level == 0 { - break; - } - } - } - - ('/', '=') => { - self.eat_next(); - return Some((Token::DivideAssign, pos)); - } - ('/', _) => return Some((Token::Divide, pos)), - - (';', _) => return Some((Token::SemiColon, pos)), - (',', _) => return Some((Token::Comma, pos)), - ('.', _) => return Some((Token::Period, pos)), - - ('=', '=') => { - self.eat_next(); - - // Warn against `===` - if self.peek_next() == Some('=') { - return Some(( - Token::LexError(Box::new(LERR::ImproperSymbol( - "'===' is not a valid operator. This is not JavaScript! Should it be '=='?" - .to_string(), - ))), - pos, - )); - } - - 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)), - - (':', ':') => { - 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(); - - return Some(( - if self.peek_next() == Some('=') { - self.eat_next(); - Token::LeftShiftAssign - } else { - Token::LeftShift - }, - pos, - )); - } - ('<', _) => return Some((Token::LessThan, pos)), - - ('>', '=') => { - self.eat_next(); - return Some((Token::GreaterThanEqualsTo, pos)); - } - ('>', '>') => { - self.eat_next(); - - return Some(( - if self.peek_next() == Some('=') { - self.eat_next(); - Token::RightShiftAssign - } else { - Token::RightShift - }, - pos, - )); - } - ('>', _) => return Some((Token::GreaterThan, pos)), - - ('!', '=') => { - self.eat_next(); - - // Warn against `!==` - if self.peek_next() == Some('=') { - return Some(( - Token::LexError(Box::new(LERR::ImproperSymbol( - "'!==' is not a valid operator. This is not JavaScript! Should it be '!='?" - .to_string(), - ))), - pos, - )); - } - - return Some((Token::NotEqualsTo, pos)); - } - ('!', _) => return Some((Token::Bang, pos)), - - ('|', '|') => { - self.eat_next(); - return Some((Token::Or, pos)); - } - ('|', '=') => { - self.eat_next(); - return Some((Token::OrAssign, pos)); - } - ('|', _) => return Some((Token::Pipe, pos)), - - ('&', '&') => { - self.eat_next(); - return Some((Token::And, pos)); - } - ('&', '=') => { - self.eat_next(); - return Some((Token::AndAssign, pos)); - } - ('&', _) => return Some((Token::Ampersand, pos)), - - ('^', '=') => { - self.eat_next(); - return Some((Token::XOrAssign, pos)); - } - ('^', _) => return Some((Token::XOr, pos)), - - ('%', '=') => { - self.eat_next(); - return Some((Token::ModuloAssign, pos)); - } - ('%', _) => return Some((Token::Modulo, pos)), - - ('~', '=') => { - self.eat_next(); - return Some((Token::PowerOfAssign, pos)); - } - ('~', _) => return Some((Token::PowerOf, pos)), - - ('\0', _) => unreachable!(), - - (ch, _) if ch.is_whitespace() => (), - (ch, _) => return Some((Token::LexError(Box::new(LERR::UnexpectedChar(ch))), pos)), - } - } - - self.advance(); - Some((Token::EOF, self.pos)) - } +} + +/// An iterator on a `Token` stream. +pub struct TokenIterator<'a> { + /// Current state. + state: TokenizeState, + /// Current position. + pos: Position, + /// Input character stream. + stream: MultiInputsStream<'a>, } impl<'a> Iterator for TokenIterator<'a> { type Item = (Token, Position); fn next(&mut self) -> Option { - self.inner_next().map(|x| { - // Save the last token - self.can_be_unary = x.0.is_next_unary(); - x - }) + get_next_token(&mut self.stream, &mut self.state, &mut self.pos) } } /// Tokenize an input text stream. pub fn lex<'a>(input: &'a [&'a str], max_string_size: usize) -> TokenIterator<'a> { TokenIterator { - max_string_size, - can_be_unary: true, + state: TokenizeState { + max_string_size, + non_unary: false, + comment_level: 0, + end_with_none: false, + include_comments: false, + }, pos: Position::new(1, 0), - streams: input.iter().map(|s| s.chars().peekable()).collect(), + stream: MultiInputsStream { + streams: input.iter().map(|s| s.chars().peekable()).collect(), + }, } } diff --git a/src/unsafe.rs b/src/unsafe.rs index 1d1ac545..fa5b271f 100644 --- a/src/unsafe.rs +++ b/src/unsafe.rs @@ -42,16 +42,6 @@ pub fn unsafe_cast_box(item: Box) -> Result, B } } -/// # DANGEROUS!!! -/// -/// A dangerous function that blindly casts a reference from one lifetime to another lifetime. -/// -/// Force-casting a a reference to another lifetime saves on allocations and string cloning, -/// but must be used with the utmost care. -pub fn unsafe_mut_cast_to_lifetime<'a, T>(value: &mut T) -> &'a mut T { - unsafe { mem::transmute::<_, &'a mut T>(value) } -} - /// # DANGEROUS!!! /// /// A dangerous function that blindly casts a `&str` from one lifetime to a `Cow` of diff --git a/tests/bool_op.rs b/tests/bool_op.rs index 913106e6..84c3d44f 100644 --- a/tests/bool_op.rs +++ b/tests/bool_op.rs @@ -39,9 +39,9 @@ fn test_bool_op_short_circuit() -> Result<(), Box> { assert_eq!( engine.eval::( r" - let this = true; + let x = true; - this || { throw; }; + x || { throw; }; " )?, true @@ -50,9 +50,9 @@ fn test_bool_op_short_circuit() -> Result<(), Box> { assert_eq!( engine.eval::( r" - let this = false; + let x = false; - this && { throw; }; + x && { throw; }; " )?, false @@ -68,9 +68,9 @@ fn test_bool_op_no_short_circuit1() { assert!(engine .eval::( r" - let this = true; + let x = true; - this | { throw; } + x | { throw; } " ) .is_err()); @@ -83,9 +83,9 @@ fn test_bool_op_no_short_circuit2() { assert!(engine .eval::( r" - let this = false; + let x = false; - this & { throw; } + x & { throw; } " ) .is_err()); diff --git a/tests/functions.rs b/tests/functions.rs index 6a94256a..49789da4 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -19,28 +19,74 @@ fn test_functions() -> Result<(), Box> { #[cfg(not(feature = "no_object"))] assert_eq!( - engine.eval::("fn add(x, n) { x + n } let x = 40; x.add(2)")?, + engine.eval::("fn add(n) { this + n } let x = 40; x.add(2)")?, 42 ); #[cfg(not(feature = "no_object"))] assert_eq!( - engine.eval::("fn add(x, n) { x += n; } let x = 40; x.add(2); x")?, - 40 + engine.eval::("fn add(n) { this += n; } let x = 40; x.add(2); x")?, + 42 ); assert_eq!(engine.eval::("fn mul2(x) { x * 2 } mul2(21)")?, 42); + assert_eq!( + engine.eval::("fn mul2(x) { x *= 2 } let a = 21; mul2(a); a")?, + 21 + ); + #[cfg(not(feature = "no_object"))] assert_eq!( - engine.eval::("fn mul2(x) { x * 2 } let x = 21; x.mul2()")?, + engine.eval::("fn mul2() { this * 2 } let x = 21; x.mul2()")?, 42 ); #[cfg(not(feature = "no_object"))] assert_eq!( - engine.eval::("fn mul2(x) { x *= 2; } let x = 21; x.mul2(); x")?, - 21 + engine.eval::("fn mul2() { this *= 2; } let x = 21; x.mul2(); x")?, + 42 + ); + + Ok(()) +} + +#[test] +fn test_function_pointers() -> Result<(), Box> { + let engine = Engine::new(); + + assert_eq!(engine.eval::(r#"type_of(Fn("abc"))"#)?, "Fn"); + + assert_eq!( + engine.eval::( + r#" + fn foo(x) { 40 + x } + + let fn_name = "f"; + fn_name += "oo"; + + let f = Fn(fn_name); + f.call(2) + "# + )?, + 42 + ); + + assert!(matches!( + *engine.eval::(r#"let f = Fn("abc"); f.call(0)"#).expect_err("should error"), + EvalAltResult::ErrorFunctionNotFound(f, _) if f.starts_with("abc (") + )); + + assert_eq!( + engine.eval::( + r#" + fn foo(x) { 40 + x } + + let x = #{ action: Fn("foo") }; + x.action.call(2) + "# + )?, + 42 ); Ok(()) diff --git a/tests/maps.rs b/tests/maps.rs index 8061f1ed..bc2b24a9 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -250,3 +250,25 @@ fn test_map_json() -> Result<(), Box> { Ok(()) } + +#[test] +#[cfg(not(feature = "no_function"))] +fn test_map_oop() -> Result<(), Box> { + let engine = Engine::new(); + + assert_eq!( + engine.eval::( + r#" + let obj = #{ data: 40, action: Fn("abc") }; + + fn abc(x) { this.data += x; } + + obj.action(2); + obj.data + "#, + )?, + 42 + ); + + Ok(()) +} diff --git a/tests/modules.rs b/tests/modules.rs index 771aa490..0e1eb3ba 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -1,9 +1,8 @@ #![cfg(not(feature = "no_module"))] use rhai::{ - module_resolvers, Dynamic, Engine, EvalAltResult, Module, ParseError, ParseErrorType, Scope, - INT, + module_resolvers::StaticModuleResolver, Engine, EvalAltResult, Module, ParseError, + ParseErrorType, Scope, INT, }; -use std::any::TypeId; #[test] fn test_module() { @@ -39,22 +38,25 @@ fn test_module_sub_module() -> Result<(), Box> { assert_eq!(m2.get_var_value::("answer").unwrap(), 41); - let engine = Engine::new(); + let mut resolver = StaticModuleResolver::new(); + resolver.insert("question", module); + + let mut engine = Engine::new(); + engine.set_module_resolver(Some(resolver)); + let mut scope = Scope::new(); - scope.push_module("question", module); - assert_eq!( - engine.eval_expression_with_scope::( + engine.eval_with_scope::( &mut scope, - "question::life::universe::answer + 1" + r#"import "question" as q; q::life::universe::answer + 1"# )?, 42 ); assert_eq!( - engine.eval_expression_with_scope::( + engine.eval_with_scope::( &mut scope, - "question::life::universe::inc(question::life::universe::answer)" + r#"import "question" as q; q::life::universe::inc(q::life::universe::answer)"# )?, 42 ); @@ -64,7 +66,7 @@ fn test_module_sub_module() -> Result<(), Box> { #[test] fn test_module_resolver() -> Result<(), Box> { - let mut resolver = module_resolvers::StaticModuleResolver::new(); + let mut resolver = StaticModuleResolver::new(); let mut module = Module::new(); module.set_var("answer", 42 as INT); @@ -72,7 +74,7 @@ fn test_module_resolver() -> Result<(), Box> { Ok(x + y + z + w) }); - resolver.insert("hello".to_string(), module); + resolver.insert("hello", module); let mut engine = Engine::new(); engine.set_module_resolver(Some(resolver)); @@ -83,7 +85,7 @@ fn test_module_resolver() -> Result<(), Box> { import "hello" as h1; import "hello" as h2; h1::sum(h2::answer, -10, 3, 7) - "# + "# )?, 42 ); @@ -104,7 +106,7 @@ fn test_module_resolver() -> Result<(), Box> { } sum - "# + "# ) .expect_err("should error"), EvalAltResult::ErrorTooManyModules(_) @@ -127,7 +129,7 @@ fn test_module_resolver() -> Result<(), Box> { } sum - "# + "# ) .expect_err("should error"), EvalAltResult::ErrorInFunctionCall(fn_name, _, _) if fn_name == "foo" @@ -145,7 +147,7 @@ fn test_module_resolver() -> Result<(), Box> { for x in range(0, 10) { foo(); } - "#, + "#, )?; } @@ -157,81 +159,83 @@ fn test_module_resolver() -> Result<(), Box> { fn test_module_from_ast() -> Result<(), Box> { let mut engine = Engine::new(); - let mut resolver = rhai::module_resolvers::StaticModuleResolver::new(); + let mut resolver1 = StaticModuleResolver::new(); let mut sub_module = Module::new(); sub_module.set_var("foo", true); - resolver.insert("another module".to_string(), sub_module); - - engine.set_module_resolver(Some(resolver)); + resolver1.insert("another module", sub_module); let ast = engine.compile( r#" - // Functions become module functions - fn calc(x) { - x + 1 - } - fn add_len(x, y) { - x + len(y) - } - private fn hidden() { - throw "you shouldn't see me!"; - } - - // Imported modules become sub-modules - import "another module" as extra; - - // Variables defined at global level become module variables - const x = 123; - let foo = 41; - let hello; - - // Final variable values become constant module variable values - foo = calc(foo); - hello = "hello, " + foo + " worlds!"; + // Functions become module functions + fn calc(x) { + x + 1 + } + fn add_len(x, y) { + x + len(y) + } + private fn hidden() { + throw "you shouldn't see me!"; + } + + // Imported modules become sub-modules + import "another module" as extra; + + // Variables defined at global level become module variables + const x = 123; + let foo = 41; + let hello; + + // Final variable values become constant module variable values + foo = calc(foo); + hello = "hello, " + foo + " worlds!"; - export - x as abc, - foo, - hello, - extra as foobar; - "#, + export + x as abc, + foo, + hello; + "#, )?; + engine.set_module_resolver(Some(resolver1)); + let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?; + let mut resolver2 = StaticModuleResolver::new(); + resolver2.insert("testing", module); + engine.set_module_resolver(Some(resolver2)); + let mut scope = Scope::new(); - scope.push_module("testing", module); - assert_eq!( - engine.eval_expression_with_scope::(&mut scope, "testing::abc")?, + engine.eval_with_scope::(&mut scope, r#"import "testing" as ttt; ttt::abc"#)?, 123 ); assert_eq!( - engine.eval_expression_with_scope::(&mut scope, "testing::foo")?, + engine.eval_with_scope::(&mut scope, r#"import "testing" as ttt; ttt::foo"#)?, 42 ); - assert!(engine.eval_expression_with_scope::(&mut scope, "testing::foobar::foo")?); + assert!(engine + .eval_with_scope::(&mut scope, r#"import "testing" as ttt; ttt::extra::foo"#)?); assert_eq!( - engine.eval_expression_with_scope::(&mut scope, "testing::hello")?, + engine.eval_with_scope::(&mut scope, r#"import "testing" as ttt; ttt::hello"#)?, "hello, 42 worlds!" ); assert_eq!( - engine.eval_expression_with_scope::(&mut scope, "testing::calc(999)")?, + engine.eval_with_scope::(&mut scope, r#"import "testing" as ttt; ttt::calc(999)"#)?, 1000 ); assert_eq!( - engine.eval_expression_with_scope::( + engine.eval_with_scope::( &mut scope, - "testing::add_len(testing::foo, testing::hello)" + r#"import "testing" as ttt; ttt::add_len(ttt::foo, ttt::hello)"# )?, 59 ); assert!(matches!( *engine - .eval_expression_with_scope::<()>(&mut scope, "testing::hidden()") + .eval_with_scope::<()>(&mut scope, r#"import "testing" as ttt; ttt::hidden()"#) .expect_err("should error"), - EvalAltResult::ErrorFunctionNotFound(fn_name, _) if fn_name == "hidden" + EvalAltResult::ErrorFunctionNotFound(fn_name, _) if fn_name == "ttt::hidden" )); Ok(()) @@ -246,6 +250,7 @@ fn test_module_export() -> Result<(), Box> { ParseError(x, _) if *x == ParseErrorType::WrongExport )); + #[cfg(not(feature = "no_function"))] assert!(matches!( engine.compile(r"fn abc(x) { export x; }").expect_err("should error"), ParseError(x, _) if *x == ParseErrorType::WrongExport diff --git a/tests/optimizer.rs b/tests/optimizer.rs index 828b0da4..2a75e916 100644 --- a/tests/optimizer.rs +++ b/tests/optimizer.rs @@ -38,19 +38,13 @@ fn test_optimizer_parse() -> Result<(), Box> { let ast = engine.compile("{ const DECISION = false; if DECISION { 42 } }")?; - assert_eq!( - format!("{:?}", ast), - "AST([], )" - ); + assert!(format!("{:?}", ast).starts_with("AST([], Module(")); engine.set_optimization_level(OptimizationLevel::Full); let ast = engine.compile("if 1 == 2 { 42 }")?; - assert_eq!( - format!("{:?}", ast), - "AST([], )" - ); + assert!(format!("{:?}", ast).starts_with("AST([], Module(")); Ok(()) } diff --git a/tests/side_effects.rs b/tests/side_effects.rs index fb364747..bee1cb2f 100644 --- a/tests/side_effects.rs +++ b/tests/side_effects.rs @@ -1,9 +1,10 @@ ///! This test simulates an external command object that is driven by a script. use rhai::{Engine, EvalAltResult, RegisterFn, Scope, INT}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, RwLock}; -/// External command. +/// Simulate a command object. struct Command { + /// Simulate an external state. state: i64, } @@ -18,25 +19,7 @@ impl Command { } } -/// Wrapper object to wrap a command object. -#[derive(Clone)] -struct CommandWrapper { - command: Arc>, -} - -impl CommandWrapper { - /// Delegate command action. - pub fn do_action(&mut self, x: i64) { - let mut command = self.command.lock().unwrap(); - let val = command.get(); - command.action(val + x); - } - /// Delegate get value action. - pub fn get_value(&mut self) -> i64 { - let command = self.command.lock().unwrap(); - command.get() - } -} +type API = Arc>; #[cfg(not(feature = "no_object"))] #[test] @@ -44,22 +27,24 @@ fn test_side_effects_command() -> Result<(), Box> { let mut engine = Engine::new(); let mut scope = Scope::new(); - // Create the command object with initial state, handled by an `Rc`. + // Create the command object with initial state, handled by an `Arc`. let command = Arc::new(Mutex::new(Command { state: 12 })); assert_eq!(command.lock().unwrap().get(), 12); - // Create the wrapper. - let wrapper = CommandWrapper { - command: command.clone(), // Notice this clones the `Rc` only - }; + // Create the API object. + let api = command.clone(); // Notice this clones the `Arc` only - // Make the wrapper a singleton in the script environment. - scope.push_constant("Command", wrapper); + // Make the API object a singleton in the script environment. + scope.push_constant("Command", api); // Register type. - engine.register_type_with_name::("CommandType"); - engine.register_fn("action", CommandWrapper::do_action); - engine.register_get("value", CommandWrapper::get_value); + engine.register_type_with_name::("CommandType"); + engine.register_fn("action", |api: &mut API, x: i64| { + let mut command = api.lock().unwrap(); + let val = command.get(); + command.action(val + x); + }); + engine.register_get("value", |command: &mut API| command.lock().unwrap().get()); assert_eq!( engine.eval_with_scope::( @@ -81,9 +66,6 @@ fn test_side_effects_command() -> Result<(), Box> { #[test] fn test_side_effects_print() -> Result<(), Box> { - use std::sync::Arc; - use std::sync::RwLock; - let result = Arc::new(RwLock::new(String::from(""))); let mut engine = Engine::new();