From 7cc1a3f5dc7dc6db5ad3f6b0a4ee233d93c0131e Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 21 Jun 2020 16:37:05 +0800 Subject: [PATCH 01/29] Change Map keys to ImmutableString to facilitate fast keys(). --- src/any.rs | 11 +++++++++-- src/engine.rs | 10 ++++++---- src/optimize.rs | 8 ++++---- src/packages/map_basic.rs | 4 ++-- src/parser.rs | 4 ++-- 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/any.rs b/src/any.rs index 6a5d9050..3b42acf8 100644 --- a/src/any.rs +++ b/src/any.rs @@ -550,8 +550,15 @@ 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), _ => Err(self.type_name()), } } @@ -615,7 +622,7 @@ impl From> for Dynamic { Self(Union::Map(Box::new( value .into_iter() - .map(|(k, v)| (k, Dynamic::from(v))) + .map(|(k, v)| (k.into(), Dynamic::from(v))) .collect(), ))) } diff --git a/src/engine.rs b/src/engine.rs index f1260824..4a473b42 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -38,7 +38,7 @@ pub type Array = Vec; /// /// Not available under the `no_object` feature. #[cfg(not(feature = "no_object"))] -pub type Map = HashMap; +pub type Map = HashMap; #[cfg(not(feature = "unchecked"))] #[cfg(debug_assertions)] @@ -1389,7 +1389,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 +1398,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(())) }) @@ -1498,7 +1498,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 { diff --git a/src/optimize.rs b/src/optimize.rs index 7895fc68..6acdcc2b 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -408,7 +408,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 +434,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 +472,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 +483,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) diff --git a/src/packages/map_basic.rs b/src/packages/map_basic.rs index 9be0fb42..1488e655 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()) diff --git a/src/parser.rs b/src/parser.rs index 5d0bd902..1f856791 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -464,7 +464,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 @@ -1196,7 +1196,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)); } } From d728ac67586a443907fdbf5cec5f64208c7fafbb Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 22 Jun 2020 00:03:45 +0800 Subject: [PATCH 02/29] Edit documentation. --- README.md | 2 +- doc/src/SUMMARY.md | 24 +++++++----- doc/src/about/related.md | 5 ++- doc/src/advanced.md | 2 +- doc/src/appendix/index.md | 6 +++ doc/src/appendix/keywords.md | 30 +++++++++++++++ doc/src/appendix/literals.md | 16 ++++++++ doc/src/appendix/operators.md | 30 +++++++++++++++ doc/src/context.json | 1 + .../engine/{optimize.md => optimize/index.md} | 2 +- doc/src/engine/optimize/optimize-levels.md | 9 +++-- doc/src/engine/optimize/reoptimize.md | 35 ++++++++++++++--- doc/src/language/constants.md | 3 +- doc/src/language/eval.md | 32 ++++++++-------- doc/src/language/for.md | 29 +++++++------- doc/src/language/functions.md | 25 ++++++++---- doc/src/language/if.md | 18 ++++++--- doc/src/language/loop.md | 8 ++++ doc/src/language/method.md | 11 ++++-- doc/src/language/modules.md | 19 +--------- doc/src/language/modules/imp-resolver.md | 4 +- doc/src/language/modules/import.md | 38 ++++++++++++++++++- doc/src/language/modules/index.md | 18 +++++++++ doc/src/language/return.md | 6 +++ doc/src/language/statements.md | 25 +++++++++--- doc/src/language/variables.md | 17 ++++++++- doc/src/language/while.md | 5 +++ doc/src/links.md | 15 +++++--- .../rust/{packages.md => packages/index.md} | 2 +- doc/src/safety.md | 32 +--------------- doc/src/safety/index.md | 35 +++++++++++++++++ doc/src/safety/max-modules.md | 3 ++ doc/src/start/{builds.md => builds/index.md} | 2 +- .../start/{examples.md => examples/index.md} | 2 +- doc/src/start/features.md | 10 +++-- doc/src/start/install.md | 8 ++-- tests/side_effects.rs | 4 +- 37 files changed, 386 insertions(+), 147 deletions(-) create mode 100644 doc/src/appendix/index.md create mode 100644 doc/src/appendix/keywords.md create mode 100644 doc/src/appendix/literals.md create mode 100644 doc/src/appendix/operators.md rename doc/src/engine/{optimize.md => optimize/index.md} (99%) create mode 100644 doc/src/language/modules/index.md rename doc/src/rust/{packages.md => packages/index.md} (98%) create mode 100644 doc/src/safety/index.md rename doc/src/start/{builds.md => builds/index.md} (86%) rename doc/src/start/{examples.md => examples/index.md} (88%) diff --git a/README.md b/README.md index b4f0853d..ac9a79a1 100644 --- a/README.md +++ b/README.md @@ -42,4 +42,4 @@ 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. diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index da62423b..2ba5e9e7 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -9,12 +9,12 @@ The Rhai Scripting Language 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. [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) + 4. [Examples](start/examples/index.md) 1. [Rust](start/examples/rust.md) 2. [Scripts](start/examples/scripts.md) 3. [Using the `Engine`](engine.md) @@ -30,7 +30,7 @@ The Rhai Scripting Language 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,7 +40,7 @@ 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) 1. [Comments](language/comments.md) @@ -72,14 +72,14 @@ The Rhai Scripting Language 1. [Function Overloading](language/overload.md) 2. [Call Method as Function](language/method.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) +6. [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) @@ -91,7 +91,7 @@ The Rhai Scripting Language 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) + 1. [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) @@ -99,3 +99,7 @@ The Rhai Scripting Language 5. [Volatility Considerations](engine/optimize/volatility.md) 6. [Subtle Semantic Changes](engine/optimize/semantics.md) 2. [Eval Statement](language/eval.md) +8. [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/related.md b/doc/src/about/related.md index b6d8d8df..f91fdb64 100644 --- a/doc/src/about/related.md +++ b/doc/src/about/related.md @@ -5,8 +5,11 @@ 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 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..f6cdd67e --- /dev/null +++ b/doc/src/appendix/keywords.md @@ -0,0 +1,30 @@ +Keywords List +============= + +{{#include ../links.md}} + +| Keyword | Description | +| :--------: | ------------------------------------- | +| `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 | +| `private` | Mark function private | +| `import` | Import module | +| `export` | Export variable | +| `as` | Alias for variable export | +| `fn` | Function definition | +| `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..1f14b9fb --- /dev/null +++ b/doc/src/appendix/literals.md @@ -0,0 +1,16 @@ +Literals Syntax +=============== + +{{#include ../links.md}} + +| Type | Literal syntax | +| :--------------------------------: | :---------------------------------------: | +| `INT` | `42`, `-123`, `0` | +| `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..4a1f4a2f --- /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..187955d4 100644 --- a/doc/src/context.json +++ b/doc/src/context.json @@ -1,4 +1,5 @@ { + "version": "0.15.1", "rootUrl": "", "rootUrlX": "/rhai" } \ No newline at end of file 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..bd173be8 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/constants.md b/doc/src/language/constants.md index 82811166..b4ced7df 100644 --- a/doc/src/language/constants.md +++ b/doc/src/language/constants.md @@ -13,7 +13,8 @@ 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/eval.md b/doc/src/language/eval.md index f79810e5..f7f9965b 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, JS, 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 ``` 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: @@ -86,11 +86,11 @@ There is even a package named [`EvalPackage`]({{rootUrl}}/rust/packages.md) whic ```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/for.md b/doc/src/language/for.md index eb2406fc..ff784d12 100644 --- a/doc/src/language/for.md +++ b/doc/src/language/for.md @@ -5,50 +5,53 @@ 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..30604a53 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,13 +52,15 @@ let x = 42; fn foo() { x } // <- syntax error: variable 'x' doesn't exist ``` -Passing Arguments by Value -------------------------- + +Arguments Passed 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). +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. This can introduce subtle bugs, if not careful, especially when using the _method-call_ style. @@ -71,6 +75,7 @@ x.change(); // de-sugars to 'change(x)' x == 500; // 'x' is NOT changed! ``` + Global Definitions Only ---------------------- @@ -92,6 +97,12 @@ 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 JS's `function` keyword. 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/loop.md b/doc/src/language/loop.md index 8b1cba8e..299ca3ff 100644 --- a/doc/src/language/loop.md +++ b/doc/src/language/loop.md @@ -3,6 +3,11 @@ 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; @@ -13,3 +18,6 @@ loop { 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..a9cdc1c7 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 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.md b/doc/src/language/modules.md index 6b616103..a55ecc05 100644 --- a/doc/src/language/modules.md +++ b/doc/src/language/modules.md @@ -1,18 +1 @@ -Modules -======= - -{{#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. - -A module is of the type `Module` and encapsulates a Rhai script together with the functions defined -by that script. - -The script text is run, variables are then selectively exposed via the [`export`] statement. -Functions defined by the script are automatically exported. - -Modules loaded within this module at the global level become _sub-modules_ and are also automatically exported. - -Other scripts can then load this module and use the variables and functions exported -as if they were defined inside the same script. +# Modules diff --git a/doc/src/language/modules/imp-resolver.md b/doc/src/language/modules/imp-resolver.md index 15de7253..a36f930b 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`]({{rootUrl}}/rust/traits.md), +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 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/index.md b/doc/src/language/modules/index.md new file mode 100644 index 00000000..cac54f3d --- /dev/null +++ b/doc/src/language/modules/index.md @@ -0,0 +1,18 @@ +Modules +======= + +{{#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. + +A module is of the type `Module` and encapsulates a Rhai script together with the functions defined +by that script. + +The script text is run, variables are then selectively exposed via the [`export`] statement. +Functions defined by the script are automatically exported. + +Modules loaded within this module at the global level become _sub-modules_ and are also automatically exported. + +Other scripts can then load this module and use the variables and functions exported +as if they were defined inside the same script. 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/variables.md b/doc/src/language/variables.md index 94f0500b..581aa9c4 100644 --- a/doc/src/language/variables.md +++ b/doc/src/language/variables.md @@ -3,6 +3,9 @@ Variables {{#include ../links.md}} +Valid Names +----------- + Variables in Rhai follow normal C naming rules (i.e. must contain only ASCII letters, digits and underscores '`_`'). Variable names must start with an ASCII letter or an underscore '`_`', must contain at least one ASCII letter, @@ -11,9 +14,21 @@ and must start with an ASCII letter before a digit. Therefore, names like '`_`', '`_42`', '`3a`' etc. are not legal variable names, but '`_c3po`' and '`r2d2`' are. Variable names are also case _sensitive_. -Variables are defined using the `let` keyword. A variable defined within a statement block is _local_ to that block. +Variable names 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, then 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..ba4e57af 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -23,8 +23,8 @@ [`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 @@ -41,6 +41,9 @@ [`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 @@ -63,9 +66,9 @@ [function]: {{rootUrl}}/language/functions.md [functions]: {{rootUrl}}/language/functions.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 [`export`]: {{rootUrl}}/language/modules/export.md [`import`]: {{rootUrl}}/language/modules/import.md @@ -80,7 +83,7 @@ [maximum size of object maps]: {{rootUrl}}/safety/max-map-size.md [progress]:/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/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/safety.md b/doc/src/safety.md index d207fe9d..1140c921 100644 --- a/doc/src/safety.md +++ b/doc/src/safety.md @@ -1,31 +1 @@ -Safety and Protection Against DoS Attacks -======================================== - -{{#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. - It may also create a large [array] or [object map] literal that exhausts all memory during parsing. - -* **CPU**: A malicous script may run an infinite tight loop that consumes all CPU cycles. - -* **Time**: A malicous script may run indefinitely, thereby blocking the calling system which is waiting for a result. - -* **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 - 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, - 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, - it is a severe security breach and may put the entire system at risk. +# Safety and Protection diff --git a/doc/src/safety/index.md b/doc/src/safety/index.md new file mode 100644 index 00000000..d49768e4 --- /dev/null +++ b/doc/src/safety/index.md @@ -0,0 +1,35 @@ +Safety and Protection Against DoS Attacks +======================================== + +{{#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. + + It may also create a large [array] or [object map] literal that exhausts all memory during parsing. + +* **CPU**: A malicous script may run an infinite tight loop that consumes all CPU cycles. + +* **Time**: A malicous script may run indefinitely, thereby blocking the calling system which is waiting for a result. + +* **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. + + Another way to cause a stack overflow is to load a [self-referencing module]({{rootUrl}}/language/modules/import.md). + +* **Overflows**: A malicous 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, + thereby putting heavy load on the file-system (or even the network if the file is not local). + + 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, + 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..d707ee64 100644 --- a/doc/src/safety/max-modules.md +++ b/doc/src/safety/max-modules.md @@ -10,6 +10,9 @@ 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]({{rootUrl}}/language/modules/import.md) +(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/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/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/features.md b/doc/src/start/features.md index fca919a5..1f67fb28 100644 --- a/doc/src/start/features.md +++ b/doc/src/start/features.md @@ -4,10 +4,12 @@ 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. -Excluding unneeded functionalities can result in smaller, faster builds -as well as more control over what a script can (or cannot) do. +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. | Feature | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -38,7 +40,7 @@ The `Cargo.toml` configuration below turns on these six features: ```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`), diff --git a/doc/src/start/install.md b/doc/src/start/install.md index c3553298..b8ee54dc 100644 --- a/doc/src/start/install.md +++ b/doc/src/start/install.md @@ -3,15 +3,15 @@ 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`: +Install the Rhai crate from [`crates.io`](https:/crates.io/crates/rhai/), start by looking up the +latest version and adding this line under `dependencies` in `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/tests/side_effects.rs b/tests/side_effects.rs index fb364747..a5725b08 100644 --- a/tests/side_effects.rs +++ b/tests/side_effects.rs @@ -44,13 +44,13 @@ 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 + command: command.clone(), // Notice this clones the `Arc` only }; // Make the wrapper a singleton in the script environment. From b08f85a8b1741937271b5b9dc00ad1c98031773e Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 22 Jun 2020 09:46:36 +0800 Subject: [PATCH 03/29] Refine side_effects test. --- tests/side_effects.rs | 48 ++++++++++++++----------------------------- 1 file changed, 15 insertions(+), 33 deletions(-) diff --git a/tests/side_effects.rs b/tests/side_effects.rs index a5725b08..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] @@ -48,18 +31,20 @@ fn test_side_effects_command() -> Result<(), Box> { 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 `Arc` 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(); From a9b168ba998d6ddf0914abcd37f74745c536f0a3 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 22 Jun 2020 22:02:49 +0800 Subject: [PATCH 04/29] Refine docs. --- README.md | 2 +- doc/src/about/features.md | 2 +- doc/src/about/non-design.md | 2 +- doc/src/appendix/literals.md | 22 +++++----- doc/src/appendix/operators.md | 50 +++++++++++------------ doc/src/language/arrays.md | 1 + doc/src/language/eval.md | 2 +- doc/src/language/functions.md | 2 +- doc/src/language/modules/imp-resolver.md | 20 ++++------ doc/src/language/num-op.md | 26 ++++++------ doc/src/language/object-maps.md | 2 +- doc/src/language/values-and-types.md | 2 +- doc/src/links.md | 1 + doc/src/rust/builtin-packages.md | 1 - doc/src/rust/custom.md | 28 ++++++------- doc/src/rust/fallible.md | 26 ++++++------ doc/src/rust/functions.md | 27 +++++-------- doc/src/rust/generic.md | 11 ++--- doc/src/rust/getters-setters.md | 2 +- doc/src/rust/indexers.md | 8 ++-- doc/src/rust/operators.md | 8 ++++ doc/src/rust/print-custom.md | 5 ++- doc/src/rust/scope.md | 51 +++++++++++------------- doc/src/safety/index.md | 14 +++---- doc/src/start/builds/minimal.md | 18 ++++++++- doc/src/start/builds/performance.md | 6 ++- doc/src/start/builds/wasm.md | 38 ++++++++++++++++-- doc/src/start/examples/rust.md | 22 +++++----- doc/src/start/examples/scripts.md | 36 ++++++++--------- doc/src/start/features.md | 38 +++++++++--------- src/lib.rs | 2 +- src/token.rs | 6 +-- 32 files changed, 260 insertions(+), 221 deletions(-) delete mode 100644 doc/src/rust/builtin-packages.md diff --git a/README.md b/README.md index ac9a79a1..7d55bdbe 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. diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 16910c63..bce5bd69 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -6,7 +6,7 @@ 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). diff --git a/doc/src/about/non-design.md b/doc/src/about/non-design.md index 2b07946e..22b70f7d 100644 --- a/doc/src/about/non-design.md +++ b/doc/src/about/non-design.md @@ -26,7 +26,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/appendix/literals.md b/doc/src/appendix/literals.md index 1f14b9fb..a308e951 100644 --- a/doc/src/appendix/literals.md +++ b/doc/src/appendix/literals.md @@ -3,14 +3,14 @@ Literals Syntax {{#include ../links.md}} -| Type | Literal syntax | -| :--------------------------------: | :---------------------------------------: | -| `INT` | `42`, `-123`, `0` | -| `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 | `()` | +| 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 index 4a1f4a2f..56782b95 100644 --- a/doc/src/appendix/operators.md +++ b/doc/src/appendix/operators.md @@ -3,28 +3,28 @@ 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 | +| 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/language/arrays.md b/doc/src/language/arrays.md index 5730d862..89870f5f 100644 --- a/doc/src/language/arrays.md +++ b/doc/src/language/arrays.md @@ -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/eval.md b/doc/src/language/eval.md index f7f9965b..babb9242 100644 --- a/doc/src/language/eval.md +++ b/doc/src/language/eval.md @@ -17,7 +17,7 @@ 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 diff --git a/doc/src/language/functions.md b/doc/src/language/functions.md index 30604a53..47a6c717 100644 --- a/doc/src/language/functions.md +++ b/doc/src/language/functions.md @@ -105,4 +105,4 @@ 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 JS's `function` keyword. +This is similar to Rust and many other modern languages, such as JavaScript's `function` keyword. diff --git a/doc/src/language/modules/imp-resolver.md b/doc/src/language/modules/imp-resolver.md index a36f930b..9634f633 100644 --- a/doc/src/language/modules/imp-resolver.md +++ b/doc/src/language/modules/imp-resolver.md @@ -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/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.md b/doc/src/language/object-maps.md index 16d6b6f0..dc1adc45 100644 --- a/doc/src/language/object-maps.md +++ b/doc/src/language/object-maps.md @@ -61,7 +61,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/values-and-types.md b/doc/src/language/values-and-types.md index 2aef02c3..b3e55cb4 100644 --- a/doc/src/language/values-and-types.md +++ b/doc/src/language/values-and-types.md @@ -14,7 +14,7 @@ The following primitive types are supported natively: | **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_ | +| **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. | diff --git a/doc/src/links.md b/doc/src/links.md index ba4e57af..e59a8d45 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -69,6 +69,7 @@ [`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 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..da9af45d 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,8 +97,9 @@ 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. +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. ```rust fn foo(ts: &mut TestStruct) -> i64 { @@ -119,8 +115,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/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/print-custom.md b/doc/src/rust/print-custom.md index 53996fe0..7205e1c3 100644 --- a/doc/src/rust/print-custom.md +++ b/doc/src/rust/print-custom.md @@ -3,8 +3,9 @@ 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 | | ----------- | ------------------------------------------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------- | 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/index.md b/doc/src/safety/index.md index d49768e4..2a06c1f4 100644 --- a/doc/src/safety/index.md +++ b/doc/src/safety/index.md @@ -8,28 +8,28 @@ resources used by a script so that it does not consume more resources that it is 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 malicous script may attempt an infinite recursive call that exhausts the call stack. +* **Stack**: A malicious 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. Another way to cause a stack overflow is to load a [self-referencing module]({{rootUrl}}/language/modules/import.md). -* **Overflows**: A malicous script may deliberately cause numeric over-flows and/or under-flows, divide by zero, and/or +* **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). 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/start/builds/minimal.md b/doc/src/start/builds/minimal.md index 57ad59c7..a76075e6 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. +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..858e4d6e 100644 --- a/doc/src/start/builds/wasm.md +++ b/doc/src/start/builds/wasm.md @@ -14,8 +14,40 @@ 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. | 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..a4c8584c 100644 --- a/doc/src/start/examples/scripts.md +++ b/doc/src/start/examples/scripts.md @@ -10,21 +10,21 @@ 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 | +| [`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 +33,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 1f67fb28..57a8a44a 100644 --- a/doc/src/start/features.md +++ b/doc/src/start/features.md @@ -11,19 +11,19 @@ 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. -| 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. | Example @@ -32,19 +32,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 = "{{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/src/lib.rs b/src/lib.rs index 648a8dce..26cba976 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`: diff --git a/src/token.rs b/src/token.rs index d5f84902..5c3d8969 100644 --- a/src/token.rs +++ b/src/token.rs @@ -677,12 +677,12 @@ impl<'a> TokenIterator<'a> { _ => unreachable!(), }); - while let Some(next_char_in_hex) = self.peek_next() { - if !valid.contains(&next_char_in_hex) { + while let Some(next_char_in_escape_seq) = self.peek_next() { + if !valid.contains(&next_char_in_escape_seq) { break; } - result.push(next_char_in_hex); + result.push(next_char_in_escape_seq); self.eat_next(); } } From 54c5c139f933c750b136f8e5d9be53ef8e83cd68 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 23 Jun 2020 10:43:24 +0800 Subject: [PATCH 05/29] Add internals feature. --- Cargo.toml | 1 + RELEASES.md | 1 + doc/src/links.md | 2 +- doc/src/start/builds/wasm.md | 9 +++++---- doc/src/start/features.md | 1 + src/lib.rs | 23 ++++++++++++++++++++++- src/parser.rs | 16 +++++++++++++++- 7 files changed, 46 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 97512851..f7ed22b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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/RELEASES.md b/RELEASES.md index 33c4418a..68175862 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -13,6 +13,7 @@ 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/links.md b/doc/src/links.md index e59a8d45..8be0ed02 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -10,8 +10,8 @@ [`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 diff --git a/doc/src/start/builds/wasm.md b/doc/src/start/builds/wasm.md index 858e4d6e..1f813ae8 100644 --- a/doc/src/start/builds/wasm.md +++ b/doc/src/start/builds/wasm.md @@ -47,7 +47,8 @@ are typically used for a WASM build: 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. | +| 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/features.md b/doc/src/start/features.md index 57a8a44a..c7cbd095 100644 --- a/doc/src/start/features.md +++ b/doc/src/start/features.md @@ -24,6 +24,7 @@ more control over what a script can (or cannot) do. | `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 diff --git a/src/lib.rs b/src/lib.rs index 26cba976..cf92bfc3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,8 +63,9 @@ //! | `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`. | +//! | `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)] @@ -125,3 +126,23 @@ pub mod module_resolvers { #[cfg(not(feature = "no_optimize"))] pub use optimize::OptimizationLevel; + +// Expose internal data structures. + +#[cfg(feature = "internals")] +pub use token::Token; + +#[cfg(feature = "internals")] +pub use parser::Expr; + +#[cfg(feature = "internals")] +pub use parser::Stmt; + +#[cfg(feature = "internals")] +pub use module::ModuleRef; + +#[cfg(feature = "internals")] +pub use utils::StaticVec; + +#[cfg(feature = "internals")] +pub use parser::ReturnType; diff --git a/src/parser.rs b/src/parser.rs index 1f856791..9deda674 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -65,20 +65,34 @@ impl AST { } /// Get the statements. + #[cfg(not(feature = "internals"))] pub(crate) fn statements(&self) -> &Vec { &self.0 } + /// Get the statements. + #[cfg(feature = "internals")] + 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 } + /// Get the internal `Module` containing all script-defined functions. + #[cfg(feature = "internals")] + pub fn lib(&self) -> &Module { + &self.1 + } + /// Merge two `AST` into one. Both `AST`'s are untouched and a new, merged, version /// is returned. /// From 3b1719e0bc1c2fb75e419194e7b10f606daf992a Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 23 Jun 2020 17:36:07 +0800 Subject: [PATCH 06/29] Introduce ChainType to prepare for more chaining operators. --- src/engine.rs | 389 +++++++++++++++++++++++++++----------------------- 1 file changed, 208 insertions(+), 181 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 4a473b42..6e993a8b 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -77,6 +77,13 @@ pub const FUNC_SETTER: &str = "set$"; pub const FUNC_INDEXER_GET: &str = "$index$get$"; pub const FUNC_INDEXER_SET: &str = "$index$set$"; +/// A type specifying the method of chaining. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +enum ChainType { + Index, + Dot, +} + /// A type that encapsulates a mutation target for an expression with side effects. #[derive(Debug)] enum Target<'a> { @@ -1011,48 +1018,59 @@ impl Engine { 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> { let is_ref = target.is_ref(); let is_value = target.is_value(); + #[inline(always)] + fn get_chain_type(expr: &Expr) -> ChainType { + match expr { + Expr::Index(_) => ChainType::Index, + Expr::Dot(_) => ChainType::Dot, + _ => unreachable!(), + } + } + // 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 next_chain = get_chain_type(rhs); + let idx_pos = idx.position(); + let this_ptr = &mut self + .get_indexed_mut(state, lib, target, idx_val, idx_pos, false)?; - 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, expr, idx_values, next_chain, 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(); - 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) { + // 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()]; - 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_name, true, 0, args, is_ref, None, 0, + ) .or_else(|err| match *err { // If there is no index setter, no need to set it back because the indexer is read-only EvalAltResult::ErrorFunctionNotFound(s, _) @@ -1062,172 +1080,181 @@ impl Engine { } _ => Err(err), })?; - } - // Indexed value is a reference - update directly - Ok(ref mut this_ptr) => { - this_ptr - .set_value(new_val.unwrap()) - .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; - } - Err(err) => match *err { - // No index getter - try to call an index setter - EvalAltResult::ErrorIndexingType(_, _) => { - let fn_name = FUNC_INDEXER_SET; - let args = - &mut [target.as_mut(), &mut idx_val2, &mut new_val.unwrap()]; - - self.exec_fn_call( - state, lib, fn_name, true, 0, args, is_ref, None, 0, - )?; } - // Error - err => return Err(Box::new(err)), - }, + // Indexed value is a reference - update directly + Ok(ref mut this_ptr) => { + this_ptr.set_value(new_val.unwrap()).map_err(|err| { + EvalAltResult::new_position(err, rhs.position()) + })?; + } + Err(err) => match *err { + // No index getter - try to call an index setter + EvalAltResult::ErrorIndexingType(_, _) => { + let fn_name = FUNC_INDEXER_SET; + let args = &mut [ + target.as_mut(), + &mut idx_val2, + &mut new_val.unwrap(), + ]; + + self.exec_fn_call( + state, lib, fn_name, true, 0, args, is_ref, None, 0, + )?; + } + // Error + err => return Err(Box::new(err)), + }, + } + Ok(Default::default()) } - Ok(Default::default()) + // xxx[rhs] + _ => self + .get_indexed_mut(state, lib, target, idx_val, pos, false) + .map(|v| (v.clone_into_dynamic(), false)), } - // 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(), + #[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 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, ) - .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(); - let index = prop.clone().into(); - self.get_indexed_mut(state, lib, target, index, *pos, false)? - } else { - unreachable!(); - }; - - self.eval_dot_index_chain_helper( - state, lib, &mut val, expr, idx_values, is_idx, 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()]; - - 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, + // 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 = ??? + 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 + 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)) + } + // {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 next_chain = get_chain_type(rhs); + + 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)? + } else { + unreachable!(); + }; + + self.eval_dot_index_chain_helper( + state, lib, &mut val, expr, idx_values, next_chain, level, new_val, ) - .map_err(|err| EvalAltResult::new_position(err, *pos))?; + .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 next_chain = get_chain_type(rhs); + let args = &mut [target.as_mut(), &mut Default::default()]; - // 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, 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, next_chain, level, new_val, + ) + .map_err(|err| EvalAltResult::new_position(err, *pos))?; + + // Feed the value back via a setter just in case it has been updated + if updated || may_be_changed { + 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, + ) .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)), })?; + } } - } - 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!(), } } @@ -1241,9 +1268,9 @@ impl Engine { 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!(), }; @@ -1272,7 +1299,7 @@ impl Engine { let this_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, this_ptr, dot_rhs, idx_values, chain_type, level, new_val, ) .map(|(v, _)| v) .map_err(|err| EvalAltResult::new_position(err, *op_pos)) @@ -1288,7 +1315,7 @@ impl Engine { let val = self.eval_expr(scope, state, lib, expr, level)?; let this_ptr = &mut val.into(); self.eval_dot_index_chain_helper( - state, lib, this_ptr, dot_rhs, idx_values, is_index, level, new_val, + state, lib, this_ptr, dot_rhs, idx_values, chain_type, level, new_val, ) .map(|(v, _)| v) .map_err(|err| EvalAltResult::new_position(err, *op_pos)) From d5eb2887fff721bd6adcf5f85a97065bea2171ce Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 23 Jun 2020 19:24:26 +0800 Subject: [PATCH 07/29] Refactor Dynamic. --- src/any.rs | 115 ++++++++++++++++++++++++----------------------------- 1 file changed, 52 insertions(+), 63 deletions(-) diff --git a/src/any.rs b/src/any.rs index 3b42acf8..47da2e04 100644 --- a/src/any.rs +++ b/src/any.rs @@ -91,13 +91,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::() @@ -311,58 +311,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. @@ -454,24 +448,24 @@ 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::Unit(value) => ::downcast_ref::(value), + Union::Bool(value) => ::downcast_ref::(value), 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::(), + .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::Module(value) => ::downcast_ref::(value.as_ref()), Union::Variant(value) => value.as_ref().as_ref().as_any().downcast_ref::(), } } @@ -481,22 +475,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::Module(value) => ::downcast_mut::(value.as_mut()), Union::Variant(value) => value.as_mut().as_mut_any().downcast_mut::(), } } @@ -590,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 { @@ -617,8 +606,8 @@ 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() From ab60c2e1d27511585c9c0029f853a4fe160b034d Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 24 Jun 2020 22:45:34 +0800 Subject: [PATCH 08/29] Add scope command to repl. --- examples/repl.rs | 8 ++++++++ src/scope.rs | 30 +++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/examples/repl.rs b/examples/repl.rs index f18a56e5..81f42158 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); diff --git a/src/scope.rs b/src/scope.rs index 97d3770e..1c8a43bd 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -444,9 +444,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> { From 58c198776f6e2a7a72d1a4a3d13492a18d92ba4f Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 25 Jun 2020 11:07:46 +0800 Subject: [PATCH 09/29] Code cleanup. --- src/api.rs | 8 ++--- src/engine.rs | 58 +++++++++++++++++------------------- src/error.rs | 2 ++ src/lib.rs | 6 ++++ src/module.rs | 6 ++-- src/optimize.rs | 2 +- src/packages/string_basic.rs | 20 ++++++------- src/parser.rs | 2 ++ src/result.rs | 1 + src/scope.rs | 2 +- src/token.rs | 4 +++ 11 files changed, 61 insertions(+), 50 deletions(-) diff --git a/src/api.rs b/src/api.rs index ebfb699c..cce53dd3 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, 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`. diff --git a/src/engine.rs b/src/engine.rs index 6e993a8b..92704b24 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -71,15 +71,16 @@ 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 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, } @@ -338,15 +339,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 } @@ -359,15 +360,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 } @@ -802,7 +803,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!( "{} [{}]", @@ -814,7 +815,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!( "{} [{}]=", @@ -1022,17 +1023,18 @@ impl Engine { 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(); - #[inline(always)] - fn get_chain_type(expr: &Expr) -> ChainType { - match expr { - Expr::Index(_) => ChainType::Index, - Expr::Dot(_) => ChainType::Dot, - _ => unreachable!(), - } - } + 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(); @@ -1046,7 +1048,6 @@ impl Engine { // xxx[idx].expr... | xxx[idx][expr]... Expr::Dot(x) | Expr::Index(x) => { let (idx, expr, pos) = x.as_ref(); - let next_chain = get_chain_type(rhs); let idx_pos = idx.position(); let this_ptr = &mut self .get_indexed_mut(state, lib, target, idx_val, idx_pos, false)?; @@ -1064,17 +1065,16 @@ impl Engine { // 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, + state, lib, FN_IDX_SET, true, 0, args, is_ref, None, 0, ) .or_else(|err| match *err { // If there is no index setter, no need to set it back because the indexer is read-only EvalAltResult::ErrorFunctionNotFound(s, _) - if s == FUNC_INDEXER_SET => + if s == FN_IDX_SET => { Ok(Default::default()) } @@ -1090,7 +1090,6 @@ impl Engine { 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, @@ -1098,7 +1097,7 @@ impl Engine { ]; self.exec_fn_call( - state, lib, fn_name, true, 0, args, is_ref, None, 0, + state, lib, FN_IDX_SET, true, 0, args, is_ref, None, 0, )?; } // Error @@ -1189,7 +1188,6 @@ impl Engine { // {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 next_chain = get_chain_type(rhs); let mut val = if let Expr::Property(p) = prop { let ((prop, _, _), _) = p.as_ref(); @@ -1207,7 +1205,6 @@ impl Engine { // xxx.prop[expr] | xxx.prop.expr Expr::Index(x) | Expr::Dot(x) => { let (prop, expr, pos) = x.as_ref(); - let next_chain = get_chain_type(rhs); let args = &mut [target.as_mut(), &mut Default::default()]; let (mut val, updated) = if let Expr::Property(p) = prop { @@ -1454,10 +1451,9 @@ 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) + self.exec_fn_call(state, lib, FN_IDX_GET, true, 0, args, is_ref, None, 0) .map(|(v, _)| v.into()) .map_err(|_| { Box::new(EvalAltResult::ErrorIndexingType( diff --git a/src/error.rs b/src/error.rs index ce8615ed..f4792f11 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,6 +7,7 @@ use crate::stdlib::{boxed::Box, char, error::Error, fmt, string::String}; /// 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 +61,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/lib.rs b/src/lib.rs index cf92bfc3..04c49e42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -130,19 +130,25 @@ pub use optimize::OptimizationLevel; // Expose internal data structures. #[cfg(feature = "internals")] +#[deprecated(note = "this type is volatile and may change")] pub use token::Token; #[cfg(feature = "internals")] +#[deprecated(note = "this type is volatile and may change")] pub use parser::Expr; #[cfg(feature = "internals")] +#[deprecated(note = "this type is volatile and may change")] pub use parser::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; #[cfg(feature = "internals")] +#[deprecated(note = "this type is volatile and may change")] pub use parser::ReturnType; diff --git a/src/module.rs b/src/module.rs index 24e51f68..caf3cb39 100644 --- a/src/module.rs +++ b/src/module.rs @@ -2,7 +2,7 @@ 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::engine::{make_getter, make_setter, Engine, FN_IDX_GET, FN_IDX_SET}; use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync}; use crate::parser::{ FnAccess, @@ -557,7 +557,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 +673,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)), diff --git a/src/optimize.rs b/src/optimize.rs index 6acdcc2b..c599a44c 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -628,7 +628,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/string_basic.rs b/src/packages/string_basic.rs index 338cf5ec..195ee33a 100644 --- a/src/packages/string_basic.rs +++ b/src/packages/string_basic.rs @@ -1,5 +1,5 @@ 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::module::FuncReturn; use crate::parser::{ImmutableString, INT}; @@ -35,14 +35,14 @@ 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, FN_TO_STRING, to_string, INT, bool, char); 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 +50,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 +67,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/parser.rs b/src/parser.rs index 9deda674..9d2d7e8d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -72,6 +72,7 @@ impl AST { /// Get the statements. #[cfg(feature = "internals")] + #[deprecated(note = "this method is volatile and may change")] pub fn statements(&self) -> &Vec { &self.0 } @@ -89,6 +90,7 @@ impl AST { /// 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 } diff --git a/src/result.rs b/src/result.rs index 760a9a5e..5fb89758 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), diff --git a/src/scope.rs b/src/scope.rs index 1c8a43bd..39827ef5 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -417,7 +417,7 @@ impl<'a> Scope<'a> { pub fn set_value(&mut self, name: &'a str, value: T) { match self.get_index(name) { None => self.push(name, value), - Some((_, EntryType::Constant)) => 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) } diff --git a/src/token.rs b/src/token.rs index 5c3d8969..76019d3c 100644 --- a/src/token.rs +++ b/src/token.rs @@ -202,9 +202,11 @@ 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), EOF, @@ -776,9 +778,11 @@ impl<'a> TokenIterator<'a> { "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"))] From aeb47efce843a10183d3d278ff7b5f8b449574c6 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 25 Jun 2020 11:07:56 +0800 Subject: [PATCH 10/29] Revise docs. --- doc/src/appendix/keywords.md | 50 ++++++++++++++++----------------- doc/src/language/constants.md | 2 ++ doc/src/language/convert.md | 4 +++ doc/src/language/for.md | 10 +++++++ doc/src/language/keywords.md | 5 ++-- doc/src/language/logic.md | 36 ++++++++++++++++++++---- doc/src/language/loop.md | 3 ++ doc/src/language/modules.md | 1 - doc/src/language/overload.md | 7 +++++ doc/src/language/print-debug.md | 3 ++ doc/src/language/variables.md | 19 ++++++++----- 11 files changed, 99 insertions(+), 41 deletions(-) delete mode 100644 doc/src/language/modules.md diff --git a/doc/src/appendix/keywords.md b/doc/src/appendix/keywords.md index f6cdd67e..55299f22 100644 --- a/doc/src/appendix/keywords.md +++ b/doc/src/appendix/keywords.md @@ -3,28 +3,28 @@ Keywords List {{#include ../links.md}} -| Keyword | Description | -| :--------: | ------------------------------------- | -| `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 | -| `private` | Mark function private | -| `import` | Import module | -| `export` | Export variable | -| `as` | Alias for variable export | -| `fn` | Function definition | -| `type_of` | Get type name of value | -| `print` | Print value | -| `debug` | Print value in debug format | -| `eval` | Evaluate script | +| 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 | | +| `private` | Mark function private | [`no_function`] | +| `import` | Import module | [`no_module`] | +| `export` | Export variable | [`no_module`] | +| `as` | Alias for variable export | [`no_module`] | +| `fn` | Function definition | [`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/language/constants.md b/doc/src/language/constants.md index b4ced7df..d88ecd4f 100644 --- a/doc/src/language/constants.md +++ b/doc/src/language/constants.md @@ -9,7 +9,9 @@ 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 ``` 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/for.md b/doc/src/language/for.md index ff784d12..6780a1cd 100644 --- a/doc/src/language/for.md +++ b/doc/src/language/for.md @@ -14,7 +14,9 @@ let s = "hello, world!"; for ch in s { if ch > 'z' { continue; } // skip to the next iteration + print(ch); + if x == '@' { break; } // break out of for loop } @@ -23,21 +25,27 @@ let array = [1, 3, 5, 7, 9, 42]; for x in array { if x > 10 { continue; } // skip to the next iteration + print(x); + 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 + print(x); + 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 + print(x); + if x == 42 { break; } // break out of for loop } @@ -47,7 +55,9 @@ let map = #{a:1, b:3, c:5, d:7, e:9}; // Property names are returned in unsorted, random order for x in keys(map) { if x > 10 { continue; } // skip to the next iteration + print(x); + if x == 42 { break; } // break out of for loop } diff --git a/doc/src/language/keywords.md b/doc/src/language/keywords.md index 48c32015..82eddce4 100644 --- a/doc/src/language/keywords.md +++ b/doc/src/language/keywords.md @@ -13,8 +13,9 @@ 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`] | +| `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..c660aa52 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 ``` @@ -51,9 +58,11 @@ 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 this() | that(); // both this() and that() are evaluated + this() & that(); // both this() and that() are evaluated ``` @@ -61,14 +70,29 @@ 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 299ca3ff..b92c3f7e 100644 --- a/doc/src/language/loop.md +++ b/doc/src/language/loop.md @@ -13,8 +13,11 @@ 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 } ``` diff --git a/doc/src/language/modules.md b/doc/src/language/modules.md deleted file mode 100644 index a55ecc05..00000000 --- a/doc/src/language/modules.md +++ /dev/null @@ -1 +0,0 @@ -# Modules diff --git a/doc/src/language/overload.md b/doc/src/language/overload.md index b83a8028..b841d00a 100644 --- a/doc/src/language/overload.md +++ b/doc/src/language/overload.md @@ -10,13 +10,20 @@ New definitions _overwrite_ previous definitions of the same name and number of ```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/variables.md b/doc/src/language/variables.md index 581aa9c4..be4abbce 100644 --- a/doc/src/language/variables.md +++ b/doc/src/language/variables.md @@ -6,15 +6,20 @@ Variables Valid Names ----------- -Variables in Rhai follow normal C naming rules (i.e. must contain only ASCII letters, digits and underscores '`_`'). +Variables in Rhai follow normal C naming rules - must contain only ASCII letters, digits and underscores '`_`', +and cannot start with a digit. -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. +For example: '`_c3po`' and '`r2d2`' are valid variable names, but '`3abc`' is not. -Therefore, names like '`_`', '`_42`', '`3a`' etc. are not legal variable names, but '`_c3po`' and '`r2d2`' are. -Variable names are also case _sensitive_. +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. -Variable names cannot be the same as a [keyword]. +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 @@ -23,7 +28,7 @@ Declare a Variable Variables are declared using the `let` keyword. Variables do not have to be given an initial value. -If none is provided, then it defaults to [`()`]. +If none is provided, it defaults to [`()`]. A variable defined within a statement block is _local_ to that block. From fe640e0e13b9c641d78caab021a54abc56a07e75 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 25 Jun 2020 18:07:57 +0800 Subject: [PATCH 11/29] Implement function pointers. --- RELEASES.md | 5 +++ doc/src/SUMMARY.md | 1 + doc/src/about/features.md | 2 + doc/src/about/non-design.md | 3 ++ doc/src/language/dynamic.md | 2 + doc/src/language/eval.md | 2 +- doc/src/language/fn-ptr.md | 56 +++++++++++++++++++++++ doc/src/language/values-and-types.md | 29 ++++++------ doc/src/links.md | 2 + doc/src/rust/packages/builtin.md | 1 + src/any.rs | 40 +++++++++++++++-- src/engine.rs | 67 +++++++++++++++++++++++----- src/fn_native.rs | 30 +++++++++++++ src/packages/fn_basic.rs | 8 ++++ src/packages/mod.rs | 3 ++ src/packages/pkg_core.rs | 2 + src/packages/string_basic.rs | 6 ++- tests/functions.rs | 41 +++++++++++++++++ 18 files changed, 269 insertions(+), 31 deletions(-) create mode 100644 doc/src/language/fn-ptr.md create mode 100644 src/packages/fn_basic.rs diff --git a/RELEASES.md b/RELEASES.md index 68175862..205491a4 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -9,6 +9,11 @@ Breaking changes * The trait function `ModuleResolver::resolve` no longer takes a `Scope` as argument. +New features +------------ + +* Support for _function pointers_ via `Fn(name)` and `Fn.call(...)` syntax - a poor man's first-class function. + Enhancements ------------ diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 2ba5e9e7..ea307d34 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -71,6 +71,7 @@ The Rhai Scripting Language 14. [Functions](language/functions.md) 1. [Function Overloading](language/overload.md) 2. [Call Method as Function](language/method.md) + 3. [Function Pointers](language/fn-ptr.md) 15. [Print and Debug](language/print-debug.md) 16. [Modules](language/modules/index.md) 1. [Export Variables, Functions and Sub-Modules](language/modules/export.md) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index bce5bd69..45958e5a 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -35,6 +35,8 @@ Dynamic * Organize code base with dynamically-loadable [modules]. +* [Function pointers]. + Safe ---- diff --git a/doc/src/about/non-design.md b/doc/src/about/non-design.md index 22b70f7d..6ab4f9ed 100644 --- a/doc/src/about/non-design.md +++ b/doc/src/about/non-design.md @@ -11,10 +11,13 @@ 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. * 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). 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 babb9242..a624403f 100644 --- a/doc/src/language/eval.md +++ b/doc/src/language/eval.md @@ -28,7 +28,7 @@ eval("{ let z = y }"); // to keep a variable local, use a statement blo 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_, diff --git a/doc/src/language/fn-ptr.md b/doc/src/language/fn-ptr.md new file mode 100644 index 00000000..fc083183 --- /dev/null +++ b/doc/src/language/fn-ptr.md @@ -0,0 +1,56 @@ +Function Pointers +================= + +It is possible to store a _function pointer_ in a variable just like a normal value. +In fact, internally a function pointer simply stores the _name_ of the function as a string. + +Call a function pointer using the `call` method, which needs to be called in method-call style. + + +Built-in Functions +------------------ + +The following standard methods (mostly defined in the [`BasicFnPackage`]({{rootUrl}}/rust/packages.md) 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)" +``` diff --git a/doc/src/language/values-and-types.md b/doc/src/language/values-and-types.md index b3e55cb4..ddadc21f 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`]({{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_ | +| **[Function pointer]** (disabled with [`no_function`]) | _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/links.md b/doc/src/links.md index 8be0ed02..a9587253 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -65,6 +65,8 @@ [function]: {{rootUrl}}/language/functions.md [functions]: {{rootUrl}}/language/functions.md +[function pointer]: {{rootUrl}}/language/fn-ptr.md +[function pointers]: {{rootUrl}}/language/fn-ptr.md [`Module`]: {{rootUrl}}/language/modules/index.md [module]: {{rootUrl}}/language/modules/index.md 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/src/any.rs b/src/any.rs index 47da2e04..0f02f0d5 100644 --- a/src/any.rs +++ b/src/any.rs @@ -1,6 +1,6 @@ //! Helper module which defines the `Any` trait to to allow dynamic value handling. -use crate::fn_native::SendSync; +use crate::fn_native::{FnPtr, SendSync}; use crate::module::Module; use crate::parser::{ImmutableString, INT}; use crate::r#unsafe::{unsafe_cast_box, unsafe_try_cast}; @@ -139,6 +139,8 @@ pub enum Union { #[cfg(not(feature = "no_object"))] Map(Box), Module(Box), + #[cfg(not(feature = "no_function"))] + FnPtr(FnPtr), Variant(Box>), } @@ -176,6 +178,8 @@ impl Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(_) => TypeId::of::(), Union::Module(_) => TypeId::of::(), + #[cfg(not(feature = "no_function"))] + Union::FnPtr(_) => TypeId::of::(), Union::Variant(value) => (***value).type_id(), } } @@ -195,6 +199,8 @@ impl Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(_) => "map", Union::Module(_) => "sub-scope", + #[cfg(not(feature = "no_function"))] + Union::FnPtr(_) => "Fn", #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] @@ -219,6 +225,8 @@ impl fmt::Display for Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(value) => write!(f, "#{:?}", value), Union::Module(value) => fmt::Debug::fmt(value, f), + #[cfg(not(feature = "no_function"))] + Union::FnPtr(value) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] @@ -243,6 +251,8 @@ impl fmt::Debug for Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(value) => write!(f, "#{:?}", value), Union::Module(value) => fmt::Debug::fmt(value, f), + #[cfg(not(feature = "no_function"))] + Union::FnPtr(value) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] @@ -267,6 +277,8 @@ impl Clone for Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(ref value) => Self(Union::Map(value.clone())), Union::Module(ref value) => Self(Union::Module(value.clone())), + #[cfg(not(feature = "no_function"))] + Union::FnPtr(ref value) => Self(Union::FnPtr(value.clone())), Union::Variant(ref value) => (***value).clone_into_dynamic(), } } @@ -396,6 +408,8 @@ impl Dynamic { #[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), + #[cfg(not(feature = "no_function"))] + Union::FnPtr(value) => unsafe_try_cast(value), Union::Variant(value) => (*value).as_box_any().downcast().map(|x| *x).ok(), } } @@ -439,6 +453,8 @@ impl Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(value) => *unsafe_cast_box::<_, T>(value).unwrap(), Union::Module(value) => *unsafe_cast_box::<_, T>(value).unwrap(), + #[cfg(not(feature = "no_function"))] + Union::FnPtr(value) => unsafe_try_cast(value).unwrap(), Union::Variant(value) => (*value).as_box_any().downcast().map(|x| *x).unwrap(), } } @@ -454,8 +470,7 @@ impl Dynamic { match &self.0 { Union::Unit(value) => ::downcast_ref::(value), Union::Bool(value) => ::downcast_ref::(value), - Union::Str(value) => (value as &dyn Any) - .downcast_ref::() + 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), @@ -466,6 +481,8 @@ impl Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(value) => ::downcast_ref::(value.as_ref()), Union::Module(value) => ::downcast_ref::(value.as_ref()), + #[cfg(not(feature = "no_function"))] + Union::FnPtr(value) => ::downcast_ref::(value), Union::Variant(value) => value.as_ref().as_ref().as_any().downcast_ref::(), } } @@ -491,6 +508,8 @@ impl Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(value) => ::downcast_mut::(value.as_mut()), Union::Module(value) => ::downcast_mut::(value.as_mut()), + #[cfg(not(feature = "no_function"))] + Union::FnPtr(value) => ::downcast_mut::(value), Union::Variant(value) => value.as_mut().as_mut_any().downcast_mut::(), } } @@ -556,6 +575,15 @@ impl Dynamic { _ => Err(self.type_name()), } } + + /// Cast the `Dynamic` as a `FnPtr` and return the function name. + /// Returns the name of the actual type if the cast fails. + pub(crate) fn as_fn_name(&self) -> Result<&str, &'static str> { + match &self.0 { + Union::FnPtr(f) => Ok(f.fn_name()), + _ => Err(self.type_name()), + } + } } impl From<()> for Dynamic { @@ -616,6 +644,12 @@ impl, T: Variant + Clone> From> for Dynam ))) } } +#[cfg(not(feature = "no_function"))] +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/engine.rs b/src/engine.rs index 92704b24..fa91e450 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -3,7 +3,8 @@ use crate::any::{Dynamic, Union, Variant}; use crate::calc_fn_hash; use crate::error::ParseErrorType; -use crate::fn_native::{CallableFunction, Callback, FnCallArgs}; +use crate::fn_native::{CallableFunction, Callback, FnCallArgs, FnPtr}; +use crate::fn_register::RegisterResultFn; use crate::module::{resolvers, Module, ModuleResolver}; use crate::optimize::OptimizationLevel; use crate::packages::{Package, PackageLibrary, PackagesCollection, StandardPackage}; @@ -11,7 +12,7 @@ use crate::parser::{Expr, FnAccess, ImmutableString, ReturnType, ScriptFnDef, St use crate::r#unsafe::{unsafe_cast_var_name_to_lifetime, unsafe_mut_cast_to_lifetime}; use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; -use crate::token::Position; +use crate::token::{Position, Token}; use crate::utils::StaticVec; #[cfg(not(feature = "no_float"))] @@ -71,11 +72,13 @@ 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 KEYWORD_FN_PTR_CALL: &str = "call"; 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$"; +pub const FN_FN_PTR: &str = "Fn"; /// A type specifying the method of chaining. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] @@ -954,6 +957,14 @@ impl Engine { false, )), + // Fn + FN_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( @@ -1124,18 +1135,31 @@ impl Engine { // 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 idx = idx_val.downcast_mut::>().unwrap(); + + // Check if it is a FnPtr call + let (fn_name, mut arg_values, hash_fn, is_native) = + if name == KEYWORD_FN_PTR_CALL && obj.is::() { + // Redirect function name + let name = obj.as_fn_name().unwrap(); + // Recalculate hash + let hash = calc_fn_hash(empty(), name, idx.len(), empty()); + // Arguments are passed as-is + (name, idx.iter_mut().collect::>(), hash, false) + } else { + // Attached object pointer in front of the arguments + ( + name.as_ref(), + once(obj).chain(idx.iter_mut()).collect::>(), + *hash, + *native, + ) + }; + let args = arg_values.as_mut(); self.exec_fn_call( - state, lib, name, *native, *hash, args, is_ref, def_val, 0, + state, lib, fn_name, is_native, hash_fn, args, is_ref, def_val, 0, ) .map_err(|err| EvalAltResult::new_position(err, *pos))? }; @@ -1697,6 +1721,27 @@ impl Engine { let ((name, native, pos), _, hash, args_expr, def_val) = x.as_ref(); let def_val = def_val.as_ref(); + // Handle Fn + if name == FN_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, state, lib, expr, level)?; + return arg_value + .take_immutable_string() + .map(|s| FnPtr::from(s).into()) + .map_err(|type_name| { + Box::new(EvalAltResult::ErrorMismatchOutputType( + type_name.into(), + Position::none(), + )) + }); + } + } + // Handle eval if name == KEYWORD_EVAL && args_expr.len() == 1 { let hash_fn = diff --git a/src/fn_native.rs b/src/fn_native.rs index b2b3aa67..f3ab7053 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,37 @@ 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 + } +} + +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; 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/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 195ee33a..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::{FN_TO_STRING, KEYWORD_DEBUG, KEYWORD_PRINT}; +use crate::fn_native::FnPtr; use crate::module::FuncReturn; use crate::parser::{ImmutableString, INT}; @@ -34,8 +35,9 @@ 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, FN_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())); diff --git a/tests/functions.rs b/tests/functions.rs index 6a94256a..c59f0717 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -45,3 +45,44 @@ fn test_functions() -> Result<(), Box> { 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(()) +} From 580a62daaf06170d3fc6b2e3d9a5fc288a81388a Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 25 Jun 2020 19:19:16 +0800 Subject: [PATCH 12/29] Add sign function. --- doc/src/language/num-fn.md | 9 +++++---- src/packages/arithmetic.rs | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/doc/src/language/num-fn.md b/doc/src/language/num-fn.md index c6df7900..1e415e11 100644 --- a/doc/src/language/num-fn.md +++ b/doc/src/language/num-fn.md @@ -9,10 +9,11 @@ Integer Functions The following standard functions (defined in the [`BasicMathPackage`]({{rootUrl}}/rust/packages.md) 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 ----------------------- 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"))] From 259b6d0fcf17dc60c151a256ccf412f5f5db3efa Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 25 Jun 2020 19:22:14 +0800 Subject: [PATCH 13/29] Add dynamic dispatch. --- doc/src/about/features.md | 2 +- doc/src/appendix/keywords.md | 52 ++++++++++++++++++---------------- doc/src/language/fn-ptr.md | 55 ++++++++++++++++++++++++++++++++++-- 3 files changed, 81 insertions(+), 28 deletions(-) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 45958e5a..5264f050 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -35,7 +35,7 @@ Dynamic * Organize code base with dynamically-loadable [modules]. -* [Function pointers]. +* Dynamic dispatch via [function pointers]. Safe ---- diff --git a/doc/src/appendix/keywords.md b/doc/src/appendix/keywords.md index 55299f22..85b9dc48 100644 --- a/doc/src/appendix/keywords.md +++ b/doc/src/appendix/keywords.md @@ -3,28 +3,30 @@ 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 | | -| `private` | Mark function private | [`no_function`] | -| `import` | Import module | [`no_module`] | -| `export` | Export variable | [`no_module`] | -| `as` | Alias for variable export | [`no_module`] | -| `fn` | Function definition | [`no_function`] | -| `type_of` | Get type name of value | | -| `print` | Print value | | -| `debug` | Print value in debug format | | -| `eval` | Evaluate script | | +| 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 | | +| `private` | Mark function private | [`no_function`] | +| `import` | Import module | [`no_module`] | +| `export` | Export variable | [`no_module`] | +| `as` | Alias for variable export | [`no_module`] | +| `fn` (lower-case `f`) | Function definition | [`no_function`] | +| `Fn` (capital `F`) | Function to create a [function pointer] | [`no_function`] | +| `call` | Call a [function pointer] | [`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/language/fn-ptr.md b/doc/src/language/fn-ptr.md index fc083183..b509ca21 100644 --- a/doc/src/language/fn-ptr.md +++ b/doc/src/language/fn-ptr.md @@ -7,8 +7,8 @@ In fact, internally a function pointer simply stores the _name_ of the function Call a function pointer using the `call` method, which needs to be called in method-call style. -Built-in Functions ------------------- +Built-in methods +---------------- The following standard methods (mostly defined in the [`BasicFnPackage`]({{rootUrl}}/rust/packages.md) but excluded if using a [raw `Engine`]) operate on [strings]: @@ -54,3 +54,54 @@ let hello = Fn(fn_name + "_world"); hello.call(0); // error: function not found - "hello_world (i64)" ``` + + +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); +``` From 175c3ccaec6c7c8a0247f87beda28bda5ba06437 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 26 Jun 2020 10:39:18 +0800 Subject: [PATCH 14/29] OOP support. --- Cargo.toml | 2 +- RELEASES.md | 8 +- doc/src/SUMMARY.md | 6 +- doc/src/about/features.md | 2 + doc/src/about/non-design.md | 1 + doc/src/appendix/keywords.md | 55 ++-- doc/src/context.json | 2 +- doc/src/language/fn-ptr.md | 2 + doc/src/language/functions.md | 67 +++-- doc/src/language/logic.md | 8 +- doc/src/language/object-maps-oop.md | 23 ++ doc/src/language/oop.md | 44 ++++ doc/src/links.md | 3 + doc/src/rust/custom.md | 5 +- src/api.rs | 15 +- src/engine.rs | 383 ++++++++++++++++++---------- src/error.rs | 8 +- src/fn_native.rs | 2 +- src/optimize.rs | 1 + src/packages/array_basic.rs | 2 +- src/packages/string_more.rs | 1 + src/parser.rs | 27 +- src/result.rs | 6 + tests/bool_op.rs | 16 +- tests/functions.rs | 17 +- tests/maps.rs | 22 ++ tests/modules.rs | 4 +- 27 files changed, 498 insertions(+), 234 deletions(-) create mode 100644 doc/src/language/object-maps-oop.md create mode 100644 doc/src/language/oop.md diff --git a/Cargo.toml b/Cargo.toml index f7ed22b4..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" diff --git a/RELEASES.md b/RELEASES.md index 205491a4..f42790be 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,18 +1,24 @@ 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. 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. Enhancements ------------ diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index ea307d34..6ea2fc76 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -56,6 +56,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) @@ -92,14 +93,15 @@ The Rhai Scripting Language 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/index.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) 8. [Appendix](appendix/index.md) 1. [Keywords](appendix/keywords.md) 2. [Operators](appendix/operators.md) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 5264f050..a079873f 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -37,6 +37,8 @@ Dynamic * Dynamic dispatch via [function pointers]. +* Some support for [OOP]. + Safe ---- diff --git a/doc/src/about/non-design.md b/doc/src/about/non-design.md index 6ab4f9ed..f93bb9ec 100644 --- a/doc/src/about/non-design.md +++ b/doc/src/about/non-design.md @@ -13,6 +13,7 @@ It doesn't attempt to be a new language. For example: * 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 [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. diff --git a/doc/src/appendix/keywords.md b/doc/src/appendix/keywords.md index 85b9dc48..6e13178f 100644 --- a/doc/src/appendix/keywords.md +++ b/doc/src/appendix/keywords.md @@ -3,30 +3,31 @@ 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 | | -| `private` | Mark function private | [`no_function`] | -| `import` | Import module | [`no_module`] | -| `export` | Export variable | [`no_module`] | -| `as` | Alias for variable export | [`no_module`] | -| `fn` (lower-case `f`) | Function definition | [`no_function`] | -| `Fn` (capital `F`) | Function to create a [function pointer] | [`no_function`] | -| `call` | Call a [function pointer] | [`no_function`] | -| `type_of` | Get type name of value | | -| `print` | Print value | | -| `debug` | Print value in debug format | | -| `eval` | Evaluate script | | +| 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] | [`no_function`] | +| `call` | Call a [function pointer] | [`no_function`] | +| `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/context.json b/doc/src/context.json index 187955d4..6da8d6bb 100644 --- a/doc/src/context.json +++ b/doc/src/context.json @@ -1,5 +1,5 @@ { - "version": "0.15.1", + "version": "0.16.0", "rootUrl": "", "rootUrlX": "/rhai" } \ No newline at end of file diff --git a/doc/src/language/fn-ptr.md b/doc/src/language/fn-ptr.md index b509ca21..a2f7f83f 100644 --- a/doc/src/language/fn-ptr.md +++ b/doc/src/language/fn-ptr.md @@ -1,6 +1,8 @@ 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. diff --git a/doc/src/language/functions.md b/doc/src/language/functions.md index 47a6c717..72dac0b4 100644 --- a/doc/src/language/functions.md +++ b/doc/src/language/functions.md @@ -53,29 +53,6 @@ fn foo() { x } // <- syntax error: variable 'x' doesn't exist ``` -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. - -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 ---------------------- @@ -106,3 +83,47 @@ 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/logic.md b/doc/src/language/logic.md index c660aa52..113641f6 100644 --- a/doc/src/language/logic.md +++ b/doc/src/language/logic.md @@ -57,13 +57,13 @@ 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 +a() || b(); // b() is not evaluated if a() is true -this() && that(); // that() is not evaluated if this() is false +a() && b(); // b() is not evaluated if a() is false -this() | that(); // both this() and that() are evaluated +a() | b(); // both a() and b() are evaluated -this() & that(); // both this() and that() are evaluated +a() & b(); // both a() and b() are evaluated ``` Compound Assignment Operators diff --git a/doc/src/language/object-maps-oop.md b/doc/src/language/object-maps-oop.md new file mode 100644 index 00000000..1a37d897 --- /dev/null +++ b/doc/src/language/object-maps-oop.md @@ -0,0 +1,23 @@ +Special Support for OOP via Object Maps +====================================== + +{{#include ../links.md}} + +[Object maps] can be used to simulate object-oriented programming ([OOP]) by storing data +as properties and methods as properties holding [function pointers]. + +If an [object map]'s property holding a [function pointer] is called in method-call style, +it calls the function referenced by the [function pointer]. + +```rust +fn do_action(x) { print(this + 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); // prints 42 + +obj.action.call(2); // <- the above de-sugars to this +``` 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/links.md b/doc/src/links.md index a9587253..8f3bcf15 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -77,6 +77,9 @@ [`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 diff --git a/doc/src/rust/custom.md b/doc/src/rust/custom.md index da9af45d..b87f6409 100644 --- a/doc/src/rust/custom.md +++ b/doc/src/rust/custom.md @@ -97,16 +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 +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' diff --git a/src/api.rs b/src/api.rs index cce53dd3..8ea047f9 100644 --- a/src/api.rs +++ b/src/api.rs @@ -996,7 +996,7 @@ impl Engine { ast.statements() .iter() .try_fold(().into(), |_, stmt| { - self.eval_stmt(scope, &mut state, ast.lib(), stmt, 0) + self.eval_stmt(scope, &mut state, ast.lib(), &mut None, stmt, 0) }) .or_else(|err| match *err { EvalAltResult::Return(out, _) => Ok(out), @@ -1062,7 +1062,7 @@ impl Engine { ast.statements() .iter() .try_fold(().into(), |_, stmt| { - self.eval_stmt(scope, &mut state, ast.lib(), stmt, 0) + self.eval_stmt(scope, &mut state, ast.lib(), &mut None, stmt, 0) }) .map_or_else( |err| match *err { @@ -1203,7 +1203,16 @@ impl Engine { let mut state = State::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 state, + ast.lib(), + &mut None, + name, + fn_def, + args, + 0, + ) } /// Optimize the `AST` with constants defined in an external Scope. diff --git a/src/engine.rs b/src/engine.rs index fa91e450..5d340e59 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -4,7 +4,6 @@ use crate::any::{Dynamic, Union, Variant}; use crate::calc_fn_hash; use crate::error::ParseErrorType; use crate::fn_native::{CallableFunction, Callback, FnCallArgs, FnPtr}; -use crate::fn_register::RegisterResultFn; use crate::module::{resolvers, Module, ModuleResolver}; use crate::optimize::OptimizationLevel; use crate::packages::{Package, PackageLibrary, PackagesCollection, StandardPackage}; @@ -12,7 +11,7 @@ use crate::parser::{Expr, FnAccess, ImmutableString, ReturnType, ScriptFnDef, St use crate::r#unsafe::{unsafe_cast_var_name_to_lifetime, unsafe_mut_cast_to_lifetime}; use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; -use crate::token::{Position, Token}; +use crate::token::Position; use crate::utils::StaticVec; #[cfg(not(feature = "no_float"))] @@ -73,6 +72,7 @@ pub const KEYWORD_DEBUG: &str = "debug"; pub const KEYWORD_TYPE_OF: &str = "type_of"; pub const KEYWORD_EVAL: &str = "eval"; 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$"; @@ -189,7 +189,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. @@ -393,13 +393,23 @@ fn default_print(s: &str) { fn search_scope<'s, 'a>( scope: &'s mut Scope, 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 @@ -654,6 +664,7 @@ impl Engine { (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> { @@ -724,9 +735,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, @@ -735,13 +746,33 @@ 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, + state, + lib, + &mut Some(first[0]), + fn_name, + fn_def, + rest, + level, + )?, + false, + )); + } else { + let result = self.call_script_fn( + scope, 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)?; @@ -860,6 +891,7 @@ impl Engine { scope: &mut Scope, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, fn_name: &str, fn_def: &ScriptFnDef, args: &mut FnCallArgs, @@ -885,7 +917,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, 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), @@ -942,6 +974,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> { @@ -977,7 +1010,8 @@ impl Engine { _ => { let mut scope = Scope::new(); self.call_fn_raw( - &mut scope, state, lib, fn_name, hashes, args, is_ref, def_val, level, + &mut scope, state, lib, fn_name, hashes, args, is_ref, is_method, def_val, + level, ) } } @@ -1027,6 +1061,7 @@ impl Engine { &self, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, target: &mut Target, rhs: &Expr, idx_values: &mut StaticVec, @@ -1060,11 +1095,12 @@ impl Engine { Expr::Dot(x) | Expr::Index(x) => { let (idx, expr, pos) = x.as_ref(); let idx_pos = idx.position(); - let this_ptr = &mut self - .get_indexed_mut(state, lib, target, idx_val, idx_pos, false)?; + let obj_ptr = &mut self + .get_indexed_mut(state, lib, target, idx_val, idx_pos, false, level)?; self.eval_dot_index_chain_helper( - state, lib, this_ptr, expr, idx_values, next_chain, level, new_val, + state, lib, this_ptr, obj_ptr, expr, idx_values, next_chain, level, + new_val, ) .map_err(|err| EvalAltResult::new_position(err, *pos)) } @@ -1072,15 +1108,16 @@ impl Engine { _ if new_val.is_some() => { let mut idx_val2 = idx_val.clone(); - match self.get_indexed_mut(state, lib, target, idx_val, pos, true) { + 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(this_ptr) if this_ptr.is_value() => { + Ok(obj_ptr) if obj_ptr.is_value() => { 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, None, 0, + 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 @@ -1093,8 +1130,8 @@ impl Engine { })?; } // Indexed value is a reference - update directly - Ok(ref mut this_ptr) => { - this_ptr.set_value(new_val.unwrap()).map_err(|err| { + Ok(ref mut obj_ptr) => { + obj_ptr.set_value(new_val.unwrap()).map_err(|err| { EvalAltResult::new_position(err, rhs.position()) })?; } @@ -1108,7 +1145,8 @@ impl Engine { ]; self.exec_fn_call( - state, lib, FN_IDX_SET, true, 0, args, is_ref, None, 0, + state, lib, FN_IDX_SET, true, 0, args, is_ref, true, None, + level, )?; } // Error @@ -1119,7 +1157,7 @@ impl Engine { } // xxx[rhs] _ => self - .get_indexed_mut(state, lib, target, idx_val, pos, false) + .get_indexed_mut(state, lib, target, idx_val, pos, false, level) .map(|v| (v.clone_into_dynamic(), false)), } } @@ -1138,29 +1176,55 @@ impl Engine { let idx = idx_val.downcast_mut::>().unwrap(); // Check if it is a FnPtr call - let (fn_name, mut arg_values, hash_fn, is_native) = - if name == KEYWORD_FN_PTR_CALL && obj.is::() { - // Redirect function name - let name = obj.as_fn_name().unwrap(); - // Recalculate hash - let hash = calc_fn_hash(empty(), name, idx.len(), empty()); - // Arguments are passed as-is - (name, idx.iter_mut().collect::>(), hash, false) - } else { - // Attached object pointer in front of the arguments - ( - name.as_ref(), - once(obj).chain(idx.iter_mut()).collect::>(), - *hash, - *native, - ) + if name == KEYWORD_FN_PTR_CALL && obj.is::() { + // Redirect function name + let fn_name = obj.as_fn_name().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 mut fn_name = name.clone(); + let mut redirected = None; + 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(name.as_ref()) { + if let Some(f) = val.downcast_ref::() { + // Remap the function name + redirected = Some(f.get_fn_name().clone()); + fn_name = redirected.as_ref().unwrap().as_str().into(); + + // Recalculate the hash based on the new function name + hash = calc_fn_hash( + empty(), + fn_name.as_ref(), + idx.len(), + empty(), + ); + } + } }; - let args = arg_values.as_mut(); + // 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(); + let fn_name = fn_name.as_ref(); - self.exec_fn_call( - state, lib, fn_name, is_native, hash_fn, args, is_ref, def_val, 0, - ) + self.exec_fn_call( + state, lib, fn_name, *native, hash, args, is_ref, true, + def_val, level, + ) + } .map_err(|err| EvalAltResult::new_position(err, *pos))? }; @@ -1179,7 +1243,7 @@ impl Engine { let ((prop, _, _), pos) = x.as_ref(); let index = prop.clone().into(); let mut val = - self.get_indexed_mut(state, lib, target, index, *pos, true)?; + self.get_indexed_mut(state, lib, target, index, *pos, true, level)?; val.set_value(new_val.unwrap()) .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; @@ -1189,7 +1253,8 @@ impl Engine { 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)?; + let val = + self.get_indexed_mut(state, lib, target, index, *pos, false, level)?; Ok((val.clone_into_dynamic(), false)) } @@ -1197,17 +1262,21 @@ impl Engine { 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)) + self.exec_fn_call( + state, lib, setter, true, 0, &mut args, is_ref, true, None, level, + ) + .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)) + self.exec_fn_call( + state, lib, getter, true, 0, &mut args, is_ref, true, None, level, + ) + .map(|(v, _)| (v, false)) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } // {xxx:map}.prop[expr] | {xxx:map}.prop.expr Expr::Index(x) | Expr::Dot(x) if target.is::() => { @@ -1216,13 +1285,14 @@ impl Engine { 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)? + self.get_indexed_mut(state, lib, target, index, *pos, false, level)? } else { unreachable!(); }; self.eval_dot_index_chain_helper( - state, lib, &mut val, expr, idx_values, next_chain, level, new_val, + state, lib, this_ptr, &mut val, expr, idx_values, next_chain, level, + new_val, ) .map_err(|err| EvalAltResult::new_position(err, *pos)) } @@ -1234,8 +1304,10 @@ impl Engine { 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))? + self.exec_fn_call( + state, lib, getter, true, 0, args, is_ref, true, None, level, + ) + .map_err(|err| EvalAltResult::new_position(err, *pos))? } else { unreachable!(); }; @@ -1244,7 +1316,8 @@ impl Engine { let (result, may_be_changed) = self .eval_dot_index_chain_helper( - state, lib, target, expr, idx_values, next_chain, level, new_val, + state, lib, this_ptr, target, expr, idx_values, next_chain, level, + new_val, ) .map_err(|err| EvalAltResult::new_position(err, *pos))?; @@ -1255,7 +1328,7 @@ impl Engine { // 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, + 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 @@ -1285,6 +1358,7 @@ impl Engine { scope: &mut Scope, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, expr: &Expr, level: usize, new_val: Option, @@ -1297,30 +1371,33 @@ impl Engine { let idx_values = &mut StaticVec::new(); - self.eval_indexed_chain(scope, state, lib, dot_rhs, idx_values, 0, level)?; + self.eval_indexed_chain(scope, 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| EvalAltResult::new_position(err, *var_pos))?; + + let (target, _, typ, pos) = search_scope(scope, 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, chain_type, 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)) @@ -1333,10 +1410,10 @@ 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, 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, chain_type, 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)) @@ -1354,6 +1431,7 @@ impl Engine { scope: &mut Scope, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, expr: &Expr, idx_values: &mut StaticVec, size: usize, @@ -1364,10 +1442,11 @@ impl Engine { 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)) - .collect::, _>>()?; + let arg_values = x + .3 + .iter() + .map(|arg_expr| self.eval_expr(scope, state, lib, this_ptr, arg_expr, level)) + .collect::, _>>()?; idx_values.push(Dynamic::from(arg_values)); } @@ -1379,15 +1458,15 @@ 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, 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, 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, state, lib, this_ptr, expr, level)?), } Ok(()) @@ -1403,6 +1482,7 @@ impl Engine { mut idx: Dynamic, idx_pos: Position, create: bool, + level: usize, ) -> Result, Box> { self.inc_operations(state)?; @@ -1477,14 +1557,16 @@ impl Engine { _ => { let type_name = self.map_type_name(val.type_name()); let args = &mut [val, &mut idx]; - self.exec_fn_call(state, lib, FN_IDX_GET, 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")] @@ -1501,6 +1583,7 @@ impl Engine { scope: &mut Scope, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, lhs: &Expr, rhs: &Expr, level: usize, @@ -1508,8 +1591,8 @@ impl Engine { self.inc_operations(state) .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; - let lhs_value = self.eval_expr(scope, state, lib, lhs, level)?; - let rhs_value = self.eval_expr(scope, state, lib, rhs, level)?; + let lhs_value = self.eval_expr(scope, state, lib, this_ptr, lhs, level)?; + let rhs_value = self.eval_expr(scope, state, lib, this_ptr, rhs, level)?; match rhs_value { #[cfg(not(feature = "no_index"))] @@ -1531,7 +1614,8 @@ impl Engine { let (r, _) = self .call_fn_raw( - &mut scope, state, lib, op, hashes, args, false, def_value, level, + &mut scope, state, lib, op, hashes, args, false, false, def_value, + level, ) .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; if r.as_bool().unwrap_or(false) { @@ -1566,6 +1650,7 @@ impl Engine { scope: &mut Scope, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, expr: &Expr, level: usize, ) -> Result> { @@ -1573,27 +1658,34 @@ impl Engine { .map_err(|err| EvalAltResult::new_position(err, 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, 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, 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, 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, state, lib, this_ptr, rhs_expr, level)?; + let (lhs_ptr, name, typ, pos) = search_scope(scope, state, this_ptr, lhs_expr)?; self.inc_operations(state) .map_err(|err| EvalAltResult::new_position(err, pos))?; @@ -1632,7 +1724,9 @@ 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))?; } @@ -1646,7 +1740,7 @@ impl Engine { // 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, state, lib, this_ptr, rhs_expr, level)?; let new_val = Some(if op.is_empty() { // Normal assignment @@ -1656,10 +1750,10 @@ 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, 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))? }); @@ -1669,14 +1763,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, 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, state, lib, this_ptr, lhs_expr, level, new_val, + ), // Error assignment to constant expr if expr.is_constant() => { Err(Box::new(EvalAltResult::ErrorAssignmentToConstant( @@ -1693,16 +1787,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, 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, 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, state, lib, this_ptr, item, level)) .collect::, _>>()?, )))), @@ -1710,7 +1808,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, state, lib, this_ptr, expr, level) .map(|val| (key.clone(), val)) }) .collect::, _>>()?, @@ -1729,7 +1827,7 @@ impl Engine { 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, state, lib, expr, level)?; + let arg_value = self.eval_expr(scope, state, lib, this_ptr, expr, level)?; return arg_value .take_immutable_string() .map(|s| FnPtr::from(s).into()) @@ -1751,7 +1849,7 @@ 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, 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())); @@ -1766,7 +1864,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; @@ -1783,18 +1881,21 @@ 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, 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, _, typ, pos) = search_scope(scope, state, this_ptr, lhs)?; match typ { ScopeEntryType::Module => unreachable!(), ScopeEntryType::Constant | ScopeEntryType::Normal => (), } + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, pos))?; + args = once(target).chain(arg_values.iter_mut()).collect(); is_ref = true; @@ -1803,7 +1904,9 @@ impl Engine { _ => { arg_values = args_expr .iter() - .map(|expr| self.eval_expr(scope, state, lib, expr, level)) + .map(|expr| { + self.eval_expr(scope, state, lib, this_ptr, expr, level) + }) .collect::>()?; args = arg_values.iter_mut().collect(); @@ -1813,7 +1916,7 @@ 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)) @@ -1826,7 +1929,7 @@ 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, state, lib, this_ptr, expr, level)) .collect::, _>>()?; let mut args: StaticVec<_> = arg_values.iter_mut().collect(); @@ -1872,8 +1975,10 @@ 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)) + self.call_script_fn( + &mut scope, state, lib, &mut None, name, fn_def, args, level, + ) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } Ok(f) => { f.get_native_fn()(self, args.as_mut()).map_err(|err| err.new_position(*pos)) @@ -1888,19 +1993,19 @@ impl Engine { } } - Expr::In(x) => self.eval_in_expr(scope, state, lib, &x.0, &x.1, level), + Expr::In(x) => self.eval_in_expr(scope, 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, 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, state, lib, this_ptr, rhs, level)? .as_bool() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("AND".into(), rhs.position()) @@ -1911,14 +2016,14 @@ impl Engine { Expr::Or(x) => { let (lhs, rhs, _) = x.as_ref(); Ok((self - .eval_expr(scope, state, lib, lhs, level)? + .eval_expr(scope, 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, state, lib, this_ptr, rhs, level)? .as_bool() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("OR".into(), rhs.position()) @@ -1942,6 +2047,7 @@ impl Engine { scope: &mut Scope, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, stmt: &Stmt, level: usize, ) -> Result> { @@ -1954,7 +2060,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, state, lib, this_ptr, expr, level)?; Ok(match expr.as_ref() { // If it is a simple assignment, erase the result at the root @@ -1969,7 +2075,7 @@ impl Engine { 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, state, lib, this_ptr, stmt, level) }); scope.rewind(prev_len); @@ -1986,14 +2092,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, 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, 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, state, lib, this_ptr, stmt, level) } else { Ok(Default::default()) } @@ -2004,8 +2110,11 @@ 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) { + match self + .eval_expr(scope, state, lib, this_ptr, expr, level)? + .as_bool() + { + Ok(true) => match self.eval_stmt(scope, state, lib, this_ptr, body, level) { Ok(_) => (), Err(err) => match *err { EvalAltResult::ErrorLoopBreak(false, _) => (), @@ -2022,7 +2131,7 @@ impl Engine { // Loop statement Stmt::Loop(body) => loop { - match self.eval_stmt(scope, state, lib, body, level) { + match self.eval_stmt(scope, state, lib, this_ptr, body, level) { Ok(_) => (), Err(err) => match *err { EvalAltResult::ErrorLoopBreak(false, _) => (), @@ -2035,7 +2144,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, state, lib, this_ptr, expr, level)?; let tid = iter_type.type_id(); if let Some(func) = self @@ -2054,7 +2163,7 @@ impl Engine { self.inc_operations(state) .map_err(|err| EvalAltResult::new_position(err, stmt.position()))?; - match self.eval_stmt(scope, state, lib, stmt, level) { + match self.eval_stmt(scope, state, lib, this_ptr, stmt, level) { Ok(_) => (), Err(err) => match *err { EvalAltResult::ErrorLoopBreak(false, _) => (), @@ -2081,7 +2190,7 @@ 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, state, lib, this_ptr, x.1.as_ref().unwrap(), level)?, (x.0).1, ))) } @@ -2093,7 +2202,8 @@ 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, 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, @@ -2110,7 +2220,8 @@ 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, 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()) @@ -2126,7 +2237,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, 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()) @@ -2145,7 +2256,7 @@ impl Engine { } if let Some(path) = self - .eval_expr(scope, state, lib, &expr, level)? + .eval_expr(scope, state, lib, this_ptr, &expr, level)? .try_cast::() { #[cfg(not(feature = "no_module"))] diff --git a/src/error.rs b/src/error.rs index f4792f11..58423ef9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,7 +3,13 @@ use crate::result::EvalAltResult; use crate::token::Position; -use crate::stdlib::{boxed::Box, char, error::Error, fmt, string::String}; +use crate::stdlib::{ + boxed::Box, + char, + error::Error, + fmt, + string::{String, ToString}, +}; /// Error when tokenizing the script text. #[derive(Debug, Eq, PartialEq, Clone, Hash)] diff --git a/src/fn_native.rs b/src/fn_native.rs index f3ab7053..e89cf334 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -132,7 +132,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/optimize.rs b/src/optimize.rs index c599a44c..7582aa6d 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -128,6 +128,7 @@ fn call_fn_with_constant_arguments( (hash_fn, 0), arg_values.iter_mut().collect::>().as_mut(), false, + false, None, 0, ) diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index 59358063..d492f52f 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -8,7 +8,7 @@ use crate::parser::{ImmutableString, INT}; use crate::result::EvalAltResult; use crate::token::Position; -use crate::stdlib::{any::TypeId, boxed::Box}; +use crate::stdlib::{any::TypeId, boxed::Box, string::ToString}; // Register array utility functions fn push(list: &mut Array, item: T) -> FuncReturn<()> { diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 46805750..a472c2dd 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -12,6 +12,7 @@ use crate::engine::Array; use crate::stdlib::{ any::TypeId, + boxed::Box, fmt::Display, format, string::{String, ToString}, diff --git a/src/parser.rs b/src/parser.rs index 9d2d7e8d..9a318849 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}; @@ -1847,19 +1847,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)? } @@ -2057,6 +2046,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 @@ -2073,7 +2072,7 @@ fn parse_let( state.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())) } diff --git a/src/result.rs b/src/result.rs index 5fb89758..06bc80d3 100644 --- a/src/result.rs +++ b/src/result.rs @@ -39,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. @@ -110,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(_) => { @@ -184,6 +187,7 @@ impl fmt::Display for EvalAltResult { Self::ErrorIndexingType(_, _) | Self::ErrorNumericIndexExpr(_) | Self::ErrorStringIndexExpr(_) + | Self::ErrorUnboundedThis(_) | Self::ErrorImportExpr(_) | Self::ErrorLogicGuard(_) | Self::ErrorFor(_) @@ -270,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) @@ -309,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/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 c59f0717..49789da4 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -19,28 +19,33 @@ 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(()) 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..e6d604b6 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -1,9 +1,7 @@ #![cfg(not(feature = "no_module"))] use rhai::{ - module_resolvers, Dynamic, Engine, EvalAltResult, Module, ParseError, ParseErrorType, Scope, - INT, + module_resolvers, Engine, EvalAltResult, Module, ParseError, ParseErrorType, Scope, INT, }; -use std::any::TypeId; #[test] fn test_module() { From 14746f94cad5c1ae3bcf6a043afcb1f5e90cc66b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 26 Jun 2020 19:32:06 +0800 Subject: [PATCH 15/29] Revise OOP section for maps. --- doc/src/language/object-maps-oop.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/doc/src/language/object-maps-oop.md b/doc/src/language/object-maps-oop.md index 1a37d897..ae61ff9e 100644 --- a/doc/src/language/object-maps-oop.md +++ b/doc/src/language/object-maps-oop.md @@ -6,18 +6,31 @@ Special Support for OOP via Object Maps [Object maps] can be used to simulate object-oriented programming ([OOP]) by storing data as properties and methods as properties holding [function pointers]. -If an [object map]'s property holding a [function pointer] is called in method-call style, -it calls the function referenced by the [function pointer]. +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 + x); } // 'this' binds to the object when called +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); // prints 42 +obj.action(2); // Short-hand syntax: prints 42 -obj.action.call(2); // <- the above de-sugars to this +// 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' ``` From 31eaf321d0237322586487cb980140216afa22eb Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 26 Jun 2020 19:44:50 +0800 Subject: [PATCH 16/29] Refactor tokenizer. --- src/lib.rs | 12 +- src/token.rs | 1330 ++++++++++++++++++++++++++------------------------ 2 files changed, 702 insertions(+), 640 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 04c49e42..8ebea050 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,15 +131,11 @@ pub use optimize::OptimizationLevel; #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] -pub use token::Token; +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; - -#[cfg(feature = "internals")] -#[deprecated(note = "this type is volatile and may change")] -pub use parser::Stmt; +pub use parser::{Expr, ReturnType, Stmt}; #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] @@ -148,7 +144,3 @@ pub use module::ModuleRef; #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] pub use utils::StaticVec; - -#[cfg(feature = "internals")] -#[deprecated(note = "this type is volatile and may change")] -pub use parser::ReturnType; diff --git a/src/token.rs b/src/token.rs index 76019d3c..1e53d8a3 100644 --- a/src/token.rs +++ b/src/token.rs @@ -209,6 +209,7 @@ pub enum Token { #[cfg(not(feature = "no_module"))] As, LexError(Box), + Comment(String), EOF, } @@ -429,24 +430,690 @@ 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, /// Can the next token be a unary operator? can_be_unary: bool, - /// Current position. - pos: Position, + /// Is the tokenizer currently inside a block comment? + comment_level: usize, + /// Return `None` at the end of the stream instead of `Some(Token::EOF)`? + end_with_none: bool, + /// Include comments? + 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; +} + +/// 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)> { + // Still inside a comment? + if state.comment_level > 0 { + let start_pos = *pos; + let mut comment = Default::default(); + scan_comment(stream, state, pos, &mut comment); + + if state.include_comments { + println!("Comment ({}): {}", start_pos, comment); + 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 = 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))), + 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.can_be_unary => return Some((Token::UnaryPlus, start_pos)), + ('+', _) => return Some((Token::Plus, start_pos)), + + ('-', '0'..='9') if state.can_be_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.can_be_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 { + Default::default() + }; + + 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 { + println!("Comment ({}): {}", start_pos, comment); + 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 { + Default::default() + }; + scan_comment(stream, state, pos, &mut comment); + + if state.include_comments { + println!("Comment ({}): {}", start_pos, comment); + 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)) + } +} + +/// An 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 { @@ -477,629 +1144,25 @@ 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_escape_seq) = self.peek_next() { - if !valid.contains(&next_char_in_escape_seq) { - break; - } - - result.push(next_char_in_escape_seq); - 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, - #[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), - }, - 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(); + get_next_token(&mut self.stream, &mut self.state, &mut self.pos).map(|x| { + // Save the last token's state + self.state.can_be_unary = x.0.is_next_unary(); x }) } @@ -1108,9 +1171,16 @@ impl<'a> Iterator for TokenIterator<'a> { /// 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, + can_be_unary: true, + 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(), + }, } } From 8c632ed928d94fe2555bb8979ffcd33407ceed69 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 26 Jun 2020 19:54:12 +0800 Subject: [PATCH 17/29] Remove println. --- src/token.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/token.rs b/src/token.rs index 1e53d8a3..90ee1d34 100644 --- a/src/token.rs +++ b/src/token.rs @@ -632,7 +632,6 @@ pub fn get_next_token( scan_comment(stream, state, pos, &mut comment); if state.include_comments { - println!("Comment ({}): {}", start_pos, comment); return Some((Token::Comment(comment), start_pos)); } } @@ -920,7 +919,6 @@ pub fn get_next_token( } if state.include_comments { - println!("Comment ({}): {}", start_pos, comment); return Some((Token::Comment(comment), start_pos)); } } @@ -937,7 +935,6 @@ pub fn get_next_token( scan_comment(stream, state, pos, &mut comment); if state.include_comments { - println!("Comment ({}): {}", start_pos, comment); return Some((Token::Comment(comment), start_pos)); } } From 74c82bbd9c4b202ede4e11fb297cc1d7b77c320e Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 26 Jun 2020 22:03:21 +0800 Subject: [PATCH 18/29] Refactor. --- src/token.rs | 50 +++++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/token.rs b/src/token.rs index 90ee1d34..8fcb99a9 100644 --- a/src/token.rs +++ b/src/token.rs @@ -434,15 +434,15 @@ impl From for String { #[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, + pub non_unary: bool, /// Is the tokenizer currently inside a block comment? - comment_level: usize, + pub comment_level: usize, /// Return `None` at the end of the stream instead of `Some(Token::EOF)`? - end_with_none: bool, + pub end_with_none: bool, /// Include comments? - include_comments: bool, + pub include_comments: bool, } /// Trait that encapsulates a peekable character input stream. @@ -620,7 +620,23 @@ fn scan_comment( } /// Get the next token. -pub fn get_next_token( +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, @@ -628,7 +644,7 @@ pub fn get_next_token( // Still inside a comment? if state.comment_level > 0 { let start_pos = *pos; - let mut comment = Default::default(); + let mut comment = String::new(); scan_comment(stream, state, pos, &mut comment); if state.include_comments { @@ -872,10 +888,10 @@ pub fn get_next_token( eat_next(stream, pos); return Some((Token::PlusAssign, start_pos)); } - ('+', _) if state.can_be_unary => return Some((Token::UnaryPlus, start_pos)), + ('+', _) if !state.non_unary => return Some((Token::UnaryPlus, start_pos)), ('+', _) => return Some((Token::Plus, start_pos)), - ('-', '0'..='9') if state.can_be_unary => negated = true, + ('-', '0'..='9') if !state.non_unary => negated = true, ('-', '0'..='9') => return Some((Token::Minus, start_pos)), ('-', '=') => { eat_next(stream, pos); @@ -887,7 +903,7 @@ pub fn get_next_token( ))), start_pos, )), - ('-', _) if state.can_be_unary => return Some((Token::UnaryMinus, start_pos)), + ('-', _) if !state.non_unary => return Some((Token::UnaryMinus, start_pos)), ('-', _) => return Some((Token::Minus, start_pos)), ('*', '=') => { @@ -903,7 +919,7 @@ pub fn get_next_token( let mut comment = if state.include_comments { "//".to_string() } else { - Default::default() + String::new() }; while let Some(c) = stream.get_next() { @@ -930,7 +946,7 @@ pub fn get_next_token( let mut comment = if state.include_comments { "/*".to_string() } else { - Default::default() + String::new() }; scan_comment(stream, state, pos, &mut comment); @@ -1103,7 +1119,7 @@ pub fn get_next_token( } } -/// An type that implements the `InputStream` trait. +/// 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. @@ -1157,11 +1173,7 @@ impl<'a> Iterator for TokenIterator<'a> { type Item = (Token, Position); fn next(&mut self) -> Option { - get_next_token(&mut self.stream, &mut self.state, &mut self.pos).map(|x| { - // Save the last token's state - self.state.can_be_unary = x.0.is_next_unary(); - x - }) + get_next_token(&mut self.stream, &mut self.state, &mut self.pos) } } @@ -1170,7 +1182,7 @@ pub fn lex<'a>(input: &'a [&'a str], max_string_size: usize) -> TokenIterator<'a TokenIterator { state: TokenizeState { max_string_size, - can_be_unary: true, + non_unary: false, comment_level: 0, end_with_none: false, include_comments: false, From ab347fa14ebb5d502e49e6e89b0f68d1ebac5392 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 26 Jun 2020 22:33:27 +0800 Subject: [PATCH 19/29] Add Module::iter_script_fn. --- src/lib.rs | 2 +- src/module.rs | 34 ++++++++++++++++++++++++++++++---- src/parser.rs | 21 ++++++++++++++++++++- src/token.rs | 2 +- 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8ebea050..1dae5104 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -135,7 +135,7 @@ pub use token::{get_next_token, parse_string_literal, InputStream, Token, Tokeni #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] -pub use parser::{Expr, ReturnType, Stmt}; +pub use parser::{Expr, ReturnType, ScriptFnDef, Stmt}; #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] diff --git a/src/module.rs b/src/module.rs index caf3cb39..48a897e2 100644 --- a/src/module.rs +++ b/src/module.rs @@ -3,7 +3,7 @@ use crate::any::{Dynamic, Variant}; use crate::calc_fn_hash; use crate::engine::{make_getter, make_setter, Engine, FN_IDX_GET, FN_IDX_SET}; -use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync}; +use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync, Shared}; use crate::parser::{ FnAccess, FnAccess::{Private, Public}, @@ -65,9 +65,26 @@ impl fmt::Debug for Module { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "", - self.variables, - self.functions.len(), + "Module(\n vars: {}\n functions: {}\n)", + self.variables + .keys() + .map(|s| s.as_str()) + .collect::>() + .join(", "), + self.iter_fn() + .map(|(name, access, _, f)| match f { + CallableFunction::Script(s) => s.to_string(), + _ => format!( + "{}{}()", + match access { + FnAccess::Public => "", + FnAccess::Private => "private ", + }, + name + ), + }) + .collect::>() + .join(", "), ) } } @@ -837,6 +854,15 @@ impl Module { self.functions.values() } + /// 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 diff --git a/src/parser.rs b/src/parser.rs index 9a318849..e19c92c0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -15,7 +15,7 @@ use crate::stdlib::{ boxed::Box, char, collections::HashMap, - format, + fmt, format, iter::empty, mem, num::NonZeroUsize, @@ -202,6 +202,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 { diff --git a/src/token.rs b/src/token.rs index 8fcb99a9..5cdc1771 100644 --- a/src/token.rs +++ b/src/token.rs @@ -620,7 +620,7 @@ fn scan_comment( } /// Get the next token. -fn get_next_token( +pub fn get_next_token( stream: &mut impl InputStream, state: &mut TokenizeState, pos: &mut Position, From 588150e21791decea21a7df1d1e2c64fae83af1a Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 26 Jun 2020 23:24:35 +0800 Subject: [PATCH 20/29] Fix test. --- tests/optimizer.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/optimizer.rs b/tests/optimizer.rs index 828b0da4..3e8bef70 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([], ")); 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([], ")); Ok(()) } From 04d2fb5001f9f20728e802bcd88c69fd2b1ced54 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 26 Jun 2020 23:24:47 +0800 Subject: [PATCH 21/29] Fix no_std. --- src/lib.rs | 2 +- src/module.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1dae5104..af902d9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -135,7 +135,7 @@ pub use token::{get_next_token, parse_string_literal, InputStream, Token, Tokeni #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] -pub use parser::{Expr, ReturnType, ScriptFnDef, Stmt}; +pub use parser::{Expr, FnAccess, ReturnType, ScriptFnDef, Stmt}; #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] diff --git a/src/module.rs b/src/module.rs index 48a897e2..21e17977 100644 --- a/src/module.rs +++ b/src/module.rs @@ -18,7 +18,7 @@ use crate::stdlib::{ any::TypeId, boxed::Box, collections::HashMap, - fmt, + fmt, format, iter::empty, mem, num::NonZeroUsize, From d509006264f9efa899a4783c737e65ae91f18839 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 27 Jun 2020 10:43:57 +0800 Subject: [PATCH 22/29] Use link redirection syntax. --- doc/src/about/features.md | 6 ++--- doc/src/language/arrays.md | 2 +- doc/src/language/eval.md | 2 +- doc/src/language/fn-ptr.md | 2 +- doc/src/language/modules/imp-resolver.md | 2 +- doc/src/language/modules/resolvers.md | 2 +- doc/src/language/num-fn.md | 4 ++-- doc/src/language/object-maps.md | 2 +- doc/src/language/string-fn.md | 2 +- doc/src/language/strings-chars.md | 2 +- doc/src/language/timestamps.md | 6 ++--- doc/src/language/values-and-types.md | 30 ++++++++++++------------ doc/src/links.md | 4 +++- doc/src/rust/print-custom.md | 4 ++-- doc/src/safety/index.md | 2 +- doc/src/safety/max-modules.md | 3 +-- doc/src/start/builds/minimal.md | 2 +- 17 files changed, 39 insertions(+), 38 deletions(-) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index a079873f..c5292a42 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -8,7 +8,7 @@ Easy * 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]({{rootUrl}}/rust/getters-setters.md), [methods][custom type] and [indexers]({{rootUrl}}/rust/indexers.md). * 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 ------- @@ -50,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/language/arrays.md b/doc/src/language/arrays.md index 89870f5f..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 | | ------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | diff --git a/doc/src/language/eval.md b/doc/src/language/eval.md index a624403f..a7e5b979 100644 --- a/doc/src/language/eval.md +++ b/doc/src/language/eval.md @@ -82,7 +82,7 @@ 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; diff --git a/doc/src/language/fn-ptr.md b/doc/src/language/fn-ptr.md index a2f7f83f..a9f2e64a 100644 --- a/doc/src/language/fn-ptr.md +++ b/doc/src/language/fn-ptr.md @@ -12,7 +12,7 @@ Call a function pointer using the `call` method, which needs to be called in met Built-in methods ---------------- -The following standard methods (mostly defined in the [`BasicFnPackage`]({{rootUrl}}/rust/packages.md) but excluded if +The following standard methods (mostly defined in the [`BasicFnPackage`][packages] but excluded if using a [raw `Engine`]) operate on [strings]: | Function | Parameter(s) | Description | diff --git a/doc/src/language/modules/imp-resolver.md b/doc/src/language/modules/imp-resolver.md index 9634f633..415abf72 100644 --- a/doc/src/language/modules/imp-resolver.md +++ b/doc/src/language/modules/imp-resolver.md @@ -7,7 +7,7 @@ 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`]({{rootUrl}}/rust/traits.md), +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 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 1e415e11..a963c7e2 100644 --- a/doc/src/language/num-fn.md +++ b/doc/src/language/num-fn.md @@ -6,7 +6,7 @@ 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 | @@ -18,7 +18,7 @@ operate on `i8`, `i16`, `i32`, `i64`, `f32` and `f64` only: 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/object-maps.md b/doc/src/language/object-maps.md index dc1adc45..5cf6ae67 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 | 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 ddadc21f..5a5ae2e0 100644 --- a/doc/src/language/values-and-types.md +++ b/doc/src/language/values-and-types.md @@ -5,21 +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_ | -| **[Function pointer]** (disabled with [`no_function`]) | _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)_ | +| 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]** (disabled with [`no_function`]) | _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/links.md b/doc/src/links.md index 8f3bcf15..ae6ef01d 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -17,6 +17,7 @@ [WASM]: {{rootUrl}}/start/builds/wasm.md [`Engine`]: {{rootUrl}}/engine/hello-world.md +[traits]: {{rootUrl}}/rust/traits.md [`private`]: {{rootUrl}}/engine/call-fn.md [`Func`]: {{rootUrl}}/engine/func.md [`eval_expression`]: {{rootUrl}}/engine/expressions.md @@ -38,6 +39,8 @@ [custom type]: {{rootUrl}}/rust/custom.md [custom types]: {{rootUrl}}/rust/custom.md +[`instant::Instant`]: https://crates.io/crates/instant + [`print`]: {{rootUrl}}/language/print-debug.md [`debug`]: {{rootUrl}}/language/print-debug.md @@ -79,7 +82,6 @@ [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 diff --git a/doc/src/rust/print-custom.md b/doc/src/rust/print-custom.md index 7205e1c3..c729c49d 100644 --- a/doc/src/rust/print-custom.md +++ b/doc/src/rust/print-custom.md @@ -10,8 +10,8 @@ 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/safety/index.md b/doc/src/safety/index.md index 2a06c1f4..22ae237d 100644 --- a/doc/src/safety/index.md +++ b/doc/src/safety/index.md @@ -21,7 +21,7 @@ The most important resources to watch out for are: 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. - Another way to cause a stack overflow is to load a [self-referencing module]({{rootUrl}}/language/modules/import.md). + 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. diff --git a/doc/src/safety/max-modules.md b/doc/src/safety/max-modules.md index d707ee64..adb1c133 100644 --- a/doc/src/safety/max-modules.md +++ b/doc/src/safety/max-modules.md @@ -10,8 +10,7 @@ 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]({{rootUrl}}/language/modules/import.md) -(i.e. cycles of modules referring to each other). +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/start/builds/minimal.md b/doc/src/start/builds/minimal.md index a76075e6..d1da0f01 100644 --- a/doc/src/start/builds/minimal.md +++ b/doc/src/start/builds/minimal.md @@ -45,7 +45,7 @@ Both of these have little code size savings. Use a Raw [`Engine`] ------------------- -[`Engine::new_raw`](#raw-engine) creates 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]({{rootUrl}}/engine/raw.md#built-in-operators) of basic arithmetic and logical operators. From 549ef6bf7ff18df0752d51d7c71ed90842f9c7b3 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 27 Jun 2020 17:34:39 +0800 Subject: [PATCH 23/29] Add oop.rhai script. --- doc/src/about/features.md | 2 +- doc/src/about/non-design.md | 3 +- doc/src/language/object-maps-oop.md | 2 +- doc/src/language/object-maps.md | 1 + doc/src/start/examples/scripts.md | 1 + scripts/oop.rhai | 45 +++++++++++++++++++++++++++++ src/packages/map_basic.rs | 11 +++++++ 7 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 scripts/oop.rhai diff --git a/doc/src/about/features.md b/doc/src/about/features.md index c5292a42..75df3def 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -37,7 +37,7 @@ Dynamic * Dynamic dispatch via [function pointers]. -* Some support for [OOP]. +* Some support for [object-oriented programming (OOP)][OOP]. Safe ---- diff --git a/doc/src/about/non-design.md b/doc/src/about/non-design.md index f93bb9ec..da3747df 100644 --- a/doc/src/about/non-design.md +++ b/doc/src/about/non-design.md @@ -13,7 +13,8 @@ It doesn't attempt to be a new language. For example: * 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 [OOP] by storing [function pointers] in [object map] properties, turning them into _methods_. + 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. diff --git a/doc/src/language/object-maps-oop.md b/doc/src/language/object-maps-oop.md index ae61ff9e..4727871b 100644 --- a/doc/src/language/object-maps-oop.md +++ b/doc/src/language/object-maps-oop.md @@ -3,7 +3,7 @@ Special Support for OOP via Object Maps {{#include ../links.md}} -[Object maps] can be used to simulate object-oriented programming ([OOP]) by storing data +[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 diff --git a/doc/src/language/object-maps.md b/doc/src/language/object-maps.md index 5cf6ae67..27220134 100644 --- a/doc/src/language/object-maps.md +++ b/doc/src/language/object-maps.md @@ -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`] | diff --git a/doc/src/start/examples/scripts.md b/doc/src/start/examples/scripts.md index a4c8584c..20836aef 100644 --- a/doc/src/start/examples/scripts.md +++ b/doc/src/start/examples/scripts.md @@ -20,6 +20,7 @@ There are also a number of examples scripts that showcase Rhai's features, all i | [`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 | +| [`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 | 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/src/packages/map_basic.rs b/src/packages/map_basic.rs index 1488e655..a86e4665 100644 --- a/src/packages/map_basic.rs +++ b/src/packages/map_basic.rs @@ -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| { From f3bde843cbe15bddac6d74050523781a4f0960e4 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 27 Jun 2020 21:19:53 +0800 Subject: [PATCH 24/29] Fix scripts and tests. --- scripts/for1.rhai | 2 +- scripts/string.rhai | 6 ++--- tests/modules.rs | 64 ++++++++++++++++++++++----------------------- 3 files changed, 36 insertions(+), 36 deletions(-) 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/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/tests/modules.rs b/tests/modules.rs index e6d604b6..6f1deb92 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -81,7 +81,7 @@ fn test_module_resolver() -> Result<(), Box> { import "hello" as h1; import "hello" as h2; h1::sum(h2::answer, -10, 3, 7) - "# + "# )?, 42 ); @@ -102,7 +102,7 @@ fn test_module_resolver() -> Result<(), Box> { } sum - "# + "# ) .expect_err("should error"), EvalAltResult::ErrorTooManyModules(_) @@ -125,7 +125,7 @@ fn test_module_resolver() -> Result<(), Box> { } sum - "# + "# ) .expect_err("should error"), EvalAltResult::ErrorInFunctionCall(fn_name, _, _) if fn_name == "foo" @@ -143,7 +143,7 @@ fn test_module_resolver() -> Result<(), Box> { for x in range(0, 10) { foo(); } - "#, + "#, )?; } @@ -164,35 +164,35 @@ fn test_module_from_ast() -> Result<(), Box> { 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, + extra as foobar; + "#, )?; let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?; From aac04a5e537941ebc1687122cb6e610f60e65581 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 27 Jun 2020 23:56:24 +0800 Subject: [PATCH 25/29] Remove modules from Scope and use separate stack. --- RELEASES.md | 1 + src/any.rs | 11 -- src/api.rs | 17 ++- src/engine.rs | 267 +++++++++++++++++++++++++++------------------ src/fn_native.rs | 11 ++ src/module.rs | 208 +++++++++++++++++++++++++++-------- src/optimize.rs | 3 +- src/parser.rs | 91 ++++++++------- src/scope.rs | 91 ++------------- tests/modules.rs | 58 +++++----- tests/optimizer.rs | 10 +- 11 files changed, 434 insertions(+), 334 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index f42790be..68eb31e4 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -12,6 +12,7 @@ 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. New features ------------ diff --git a/src/any.rs b/src/any.rs index 0f02f0d5..d99712e1 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::{FnPtr, SendSync}; -use crate::module::Module; use crate::parser::{ImmutableString, INT}; use crate::r#unsafe::{unsafe_cast_box, unsafe_try_cast}; @@ -138,7 +137,6 @@ pub enum Union { Array(Box), #[cfg(not(feature = "no_object"))] Map(Box), - Module(Box), #[cfg(not(feature = "no_function"))] FnPtr(FnPtr), Variant(Box>), @@ -177,7 +175,6 @@ impl Dynamic { Union::Array(_) => TypeId::of::(), #[cfg(not(feature = "no_object"))] Union::Map(_) => TypeId::of::(), - Union::Module(_) => TypeId::of::(), #[cfg(not(feature = "no_function"))] Union::FnPtr(_) => TypeId::of::(), Union::Variant(value) => (***value).type_id(), @@ -198,7 +195,6 @@ impl Dynamic { Union::Array(_) => "array", #[cfg(not(feature = "no_object"))] Union::Map(_) => "map", - Union::Module(_) => "sub-scope", #[cfg(not(feature = "no_function"))] Union::FnPtr(_) => "Fn", @@ -224,7 +220,6 @@ 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), #[cfg(not(feature = "no_function"))] Union::FnPtr(value) => fmt::Display::fmt(value, f), @@ -250,7 +245,6 @@ 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), #[cfg(not(feature = "no_function"))] Union::FnPtr(value) => fmt::Display::fmt(value, f), @@ -276,7 +270,6 @@ impl Clone for Dynamic { Union::Array(ref value) => Self(Union::Array(value.clone())), #[cfg(not(feature = "no_object"))] Union::Map(ref value) => Self(Union::Map(value.clone())), - Union::Module(ref value) => Self(Union::Module(value.clone())), #[cfg(not(feature = "no_function"))] Union::FnPtr(ref value) => Self(Union::FnPtr(value.clone())), Union::Variant(ref value) => (***value).clone_into_dynamic(), @@ -407,7 +400,6 @@ impl Dynamic { Union::Array(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), #[cfg(not(feature = "no_object"))] Union::Map(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), - Union::Module(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), #[cfg(not(feature = "no_function"))] Union::FnPtr(value) => unsafe_try_cast(value), Union::Variant(value) => (*value).as_box_any().downcast().map(|x| *x).ok(), @@ -452,7 +444,6 @@ impl Dynamic { Union::Array(value) => *unsafe_cast_box::<_, T>(value).unwrap(), #[cfg(not(feature = "no_object"))] Union::Map(value) => *unsafe_cast_box::<_, T>(value).unwrap(), - Union::Module(value) => *unsafe_cast_box::<_, T>(value).unwrap(), #[cfg(not(feature = "no_function"))] Union::FnPtr(value) => unsafe_try_cast(value).unwrap(), Union::Variant(value) => (*value).as_box_any().downcast().map(|x| *x).unwrap(), @@ -480,7 +471,6 @@ impl Dynamic { Union::Array(value) => ::downcast_ref::(value.as_ref()), #[cfg(not(feature = "no_object"))] Union::Map(value) => ::downcast_ref::(value.as_ref()), - Union::Module(value) => ::downcast_ref::(value.as_ref()), #[cfg(not(feature = "no_function"))] Union::FnPtr(value) => ::downcast_ref::(value), Union::Variant(value) => value.as_ref().as_ref().as_any().downcast_ref::(), @@ -507,7 +497,6 @@ impl Dynamic { Union::Array(value) => ::downcast_mut::(value.as_mut()), #[cfg(not(feature = "no_object"))] Union::Map(value) => ::downcast_mut::(value.as_mut()), - Union::Module(value) => ::downcast_mut::(value.as_mut()), #[cfg(not(feature = "no_function"))] Union::FnPtr(value) => ::downcast_mut::(value), Union::Variant(value) => value.as_mut().as_mut_any().downcast_mut::(), diff --git a/src/api.rs b/src/api.rs index 8ea047f9..66050214 100644 --- a/src/api.rs +++ b/src/api.rs @@ -2,7 +2,7 @@ use crate::any::{Dynamic, Variant}; use crate::engine::{ - get_script_function_by_signature, make_getter, make_setter, Engine, State, FN_IDX_GET, + get_script_function_by_signature, make_getter, make_setter, Engine, Imports, State, FN_IDX_GET, FN_IDX_SET, }; use crate::error::ParseError; @@ -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()); @@ -986,17 +987,18 @@ impl Engine { } /// Evaluate an `AST` with own scope. - pub(crate) fn eval_ast_with_scope_raw( + 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(), &mut None, 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), @@ -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(), &mut None, stmt, 0) + self.eval_stmt(scope, &mut mods, &mut state, ast.lib(), &mut None, stmt, 0) }) .map_or_else( |err| match *err { @@ -1201,10 +1204,12 @@ impl Engine { })?; let mut state = State::new(); + let mut mods = Imports::new(); let args = args.as_mut(); self.call_script_fn( scope, + &mut mods, &mut state, ast.lib(), &mut None, diff --git a/src/engine.rs b/src/engine.rs index 5d340e59..49c5650c 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -19,6 +19,7 @@ use crate::parser::FLOAT; use crate::stdlib::{ any::TypeId, + borrow::Cow, boxed::Box, collections::HashMap, format, @@ -40,6 +41,9 @@ pub type Array = Vec; #[cfg(not(feature = "no_object"))] 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)] pub const MAX_CALL_STACK_DEPTH: usize = 16; @@ -308,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), @@ -392,6 +396,7 @@ fn default_print(s: &str) { /// 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, @@ -420,17 +425,16 @@ fn search_scope<'s, 'a>( }; let module = if let Some(index) = index { - scope - .get_mut(scope.len() - index.get()) - .0 - .downcast_mut::() - .unwrap() + let offset = mods.len() - index.get(); + &mut mods.get_mut(offset).unwrap().1 } else { // Find the root module in the scope let (id, root_pos) = modules.get(0); - scope - .find_module_internal(id) + mods.iter_mut() + .rev() + .find(|(n, _)| n == id) + .map(|(_, m)| m) .ok_or_else(|| Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)))? }; @@ -473,7 +477,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, @@ -658,6 +662,7 @@ impl Engine { pub(crate) fn call_fn_raw( &self, scope: &mut Scope, + mods: &mut Imports, state: &mut State, lib: &Module, fn_name: &str, @@ -753,6 +758,7 @@ impl Engine { return Ok(( self.call_script_fn( scope, + mods, state, lib, &mut Some(first[0]), @@ -765,7 +771,7 @@ impl Engine { )); } else { let result = self.call_script_fn( - scope, state, lib, &mut None, fn_name, fn_def, args, level, + scope, mods, state, lib, &mut None, fn_name, fn_def, args, level, )?; // Restore the original reference @@ -889,6 +895,7 @@ 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>, @@ -900,7 +907,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 @@ -917,7 +925,7 @@ impl Engine { // Evaluate the function at one higher level of call depth let result = self - .eval_stmt(scope, state, lib, this_ptr, &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), @@ -936,7 +944,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 @@ -1009,9 +1018,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, is_method, def_val, - level, + &mut scope, &mut mods, state, lib, fn_name, hashes, args, is_ref, is_method, + def_val, level, ) } } @@ -1022,6 +1032,7 @@ impl Engine { fn eval_script_expr( &self, scope: &mut Scope, + mods: &mut Imports, state: &mut State, lib: &Module, script: &Dynamic, @@ -1047,7 +1058,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)?; @@ -1356,6 +1367,7 @@ 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>, @@ -1371,7 +1383,9 @@ impl Engine { let idx_values = &mut StaticVec::new(); - self.eval_indexed_chain(scope, state, lib, this_ptr, 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[???] @@ -1381,11 +1395,10 @@ impl Engine { self.inc_operations(state) .map_err(|err| EvalAltResult::new_position(err, *var_pos))?; - let (target, _, typ, pos) = search_scope(scope, state, this_ptr, dot_lhs)?; + 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( var_name.to_string(), @@ -1410,7 +1423,7 @@ impl Engine { } // {expr}.??? or {expr}[???] expr => { - let val = self.eval_expr(scope, state, lib, this_ptr, expr, level)?; + 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, obj_ptr, dot_rhs, idx_values, chain_type, level, new_val, @@ -1429,6 +1442,7 @@ impl Engine { fn eval_indexed_chain( &self, scope: &mut Scope, + mods: &mut Imports, state: &mut State, lib: &Module, this_ptr: &mut Option<&mut Dynamic>, @@ -1442,11 +1456,12 @@ impl Engine { 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, this_ptr, arg_expr, level)) - .collect::, _>>()?; + let arg_values = + x.3.iter() + .map(|arg_expr| { + self.eval_expr(scope, mods, state, lib, this_ptr, arg_expr, level) + }) + .collect::, _>>()?; idx_values.push(Dynamic::from(arg_values)); } @@ -1458,15 +1473,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, this_ptr, lhs, level)?, + _ => self.eval_expr(scope, mods, state, lib, this_ptr, lhs, level)?, }; // Push in reverse order - self.eval_indexed_chain(scope, state, lib, this_ptr, 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, this_ptr, expr, level)?), + _ => idx_values.push(self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?), } Ok(()) @@ -1581,6 +1598,7 @@ impl Engine { fn eval_in_expr( &self, scope: &mut Scope, + mods: &mut Imports, state: &mut State, lib: &Module, this_ptr: &mut Option<&mut Dynamic>, @@ -1591,8 +1609,8 @@ impl Engine { self.inc_operations(state) .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; - let lhs_value = self.eval_expr(scope, state, lib, this_ptr, lhs, level)?; - let rhs_value = self.eval_expr(scope, state, lib, this_ptr, 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"))] @@ -1614,8 +1632,8 @@ impl Engine { let (r, _) = self .call_fn_raw( - &mut scope, state, lib, op, hashes, args, false, 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()))?; if r.as_bool().unwrap_or(false) { @@ -1648,6 +1666,7 @@ impl Engine { fn eval_expr( &self, scope: &mut Scope, + mods: &mut Imports, state: &mut State, lib: &Module, this_ptr: &mut Option<&mut Dynamic>, @@ -1658,7 +1677,7 @@ impl Engine { .map_err(|err| EvalAltResult::new_position(err, expr.position()))?; let result = match expr { - Expr::Expr(x) => self.eval_expr(scope, state, lib, this_ptr, 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"))] @@ -1673,19 +1692,21 @@ impl Engine { } } Expr::Variable(_) => { - let (val, _, _, _) = search_scope(scope, state, this_ptr, 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, this_ptr, &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, this_ptr, rhs_expr, level)?; - let (lhs_ptr, name, typ, pos) = search_scope(scope, state, this_ptr, 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))?; @@ -1732,15 +1753,14 @@ impl Engine { } 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, this_ptr, 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 @@ -1750,7 +1770,7 @@ 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, this_ptr, 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, false, None, level) @@ -1764,12 +1784,12 @@ impl Engine { // idx_lhs[idx_expr] op= rhs #[cfg(not(feature = "no_index"))] Expr::Index(_) => self.eval_dot_index_chain( - scope, state, lib, this_ptr, lhs_expr, level, new_val, + 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, this_ptr, lhs_expr, level, new_val, + scope, mods, state, lib, this_ptr, lhs_expr, level, new_val, ), // Error assignment to constant expr if expr.is_constant() => { @@ -1788,19 +1808,19 @@ impl Engine { // lhs[idx_expr] #[cfg(not(feature = "no_index"))] Expr::Index(_) => { - self.eval_dot_index_chain(scope, state, lib, this_ptr, expr, level, None) + 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, this_ptr, expr, level, None) + 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, this_ptr, item, level)) + .map(|item| self.eval_expr(scope, mods, state, lib, this_ptr, item, level)) .collect::, _>>()?, )))), @@ -1808,7 +1828,7 @@ impl Engine { Expr::Map(x) => Ok(Dynamic(Union::Map(Box::new( x.0.iter() .map(|((key, _), expr)| { - self.eval_expr(scope, state, lib, this_ptr, expr, level) + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) .map(|val| (key.clone(), val)) }) .collect::, _>>()?, @@ -1827,7 +1847,8 @@ impl Engine { 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, state, lib, this_ptr, expr, level)?; + let arg_value = + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; return arg_value .take_immutable_string() .map(|s| FnPtr::from(s).into()) @@ -1849,9 +1870,10 @@ 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, this_ptr, 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) + .eval_script_expr(scope, mods, state, lib, &script) .map_err(|err| EvalAltResult::new_position(err, expr.position())); if scope.len() != prev_len { @@ -1882,16 +1904,12 @@ impl Engine { .iter() .skip(1) .map(|expr| { - self.eval_expr(scope, state, lib, this_ptr, expr, level) + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) }) .collect::>()?; - let (target, _, typ, pos) = search_scope(scope, state, this_ptr, lhs)?; - - match typ { - ScopeEntryType::Module => unreachable!(), - ScopeEntryType::Constant | ScopeEntryType::Normal => (), - } + let (target, _, _, pos) = + search_scope(scope, mods, state, this_ptr, lhs)?; self.inc_operations(state) .map_err(|err| EvalAltResult::new_position(err, pos))?; @@ -1905,7 +1923,7 @@ impl Engine { arg_values = args_expr .iter() .map(|expr| { - self.eval_expr(scope, state, lib, this_ptr, expr, level) + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) }) .collect::>()?; @@ -1929,23 +1947,30 @@ impl Engine { let mut arg_values = args_expr .iter() - .map(|expr| self.eval_expr(scope, state, lib, this_ptr, 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() + let index = if state.always_search { + None } else { - scope.find_module_internal(id).ok_or_else(|| { - Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)) - })? + modules.index() + }; + + let module = 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 == id) + .map(|(_, m)| m) + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)) + })? }; // First search in script-defined functions (can override built-in) @@ -1975,8 +2000,9 @@ impl Engine { let args = args.as_mut(); let fn_def = f.get_fn_def(); let mut scope = Scope::new(); + let mut mods = Imports::new(); self.call_script_fn( - &mut scope, state, lib, &mut None, name, fn_def, args, level, + &mut scope, &mut mods, state, lib, &mut None, name, fn_def, args, level, ) .map_err(|err| EvalAltResult::new_position(err, *pos)) } @@ -1993,19 +2019,19 @@ impl Engine { } } - Expr::In(x) => self.eval_in_expr(scope, state, lib, this_ptr, &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, this_ptr, 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, this_ptr, rhs, level)? + .eval_expr(scope, mods, state, lib, this_ptr, rhs, level)? .as_bool() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("AND".into(), rhs.position()) @@ -2016,14 +2042,14 @@ impl Engine { Expr::Or(x) => { let (lhs, rhs, _) = x.as_ref(); Ok((self - .eval_expr(scope, state, lib, this_ptr, 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, this_ptr, rhs, level)? + .eval_expr(scope, mods, state, lib, this_ptr, rhs, level)? .as_bool() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("OR".into(), rhs.position()) @@ -2045,6 +2071,7 @@ impl Engine { pub(crate) fn eval_stmt( &self, scope: &mut Scope, + mods: &mut Imports, state: &mut State, lib: &Module, this_ptr: &mut Option<&mut Dynamic>, @@ -2060,7 +2087,7 @@ impl Engine { // Expression as statement Stmt::Expr(expr) => { - let result = self.eval_expr(scope, state, lib, this_ptr, 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 @@ -2071,14 +2098,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, this_ptr, 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 @@ -2092,14 +2121,14 @@ impl Engine { Stmt::IfThenElse(x) => { let (expr, if_block, else_block) = x.as_ref(); - self.eval_expr(scope, state, lib, this_ptr, 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, this_ptr, 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, this_ptr, stmt, level) + self.eval_stmt(scope, mods, state, lib, this_ptr, stmt, level) } else { Ok(Default::default()) } @@ -2111,17 +2140,21 @@ impl Engine { let (expr, body) = x.as_ref(); match self - .eval_expr(scope, state, lib, this_ptr, expr, level)? + .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? .as_bool() { - Ok(true) => match self.eval_stmt(scope, 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(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()))) @@ -2131,7 +2164,7 @@ impl Engine { // Loop statement Stmt::Loop(body) => loop { - match self.eval_stmt(scope, state, lib, this_ptr, body, level) { + match self.eval_stmt(scope, mods, state, lib, this_ptr, body, level) { Ok(_) => (), Err(err) => match *err { EvalAltResult::ErrorLoopBreak(false, _) => (), @@ -2144,7 +2177,7 @@ impl Engine { // For loop Stmt::For(x) => { let (name, expr, stmt) = x.as_ref(); - let iter_type = self.eval_expr(scope, state, lib, this_ptr, 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 @@ -2163,7 +2196,7 @@ impl Engine { self.inc_operations(state) .map_err(|err| EvalAltResult::new_position(err, stmt.position()))?; - match self.eval_stmt(scope, state, lib, this_ptr, stmt, level) { + match self.eval_stmt(scope, mods, state, lib, this_ptr, stmt, level) { Ok(_) => (), Err(err) => match *err { EvalAltResult::ErrorLoopBreak(false, _) => (), @@ -2190,7 +2223,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, this_ptr, x.1.as_ref().unwrap(), level)?, + self.eval_expr( + scope, + mods, + state, + lib, + this_ptr, + x.1.as_ref().unwrap(), + level, + )?, (x.0).1, ))) } @@ -2202,8 +2243,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, this_ptr, 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, @@ -2220,8 +2268,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, this_ptr, 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()) @@ -2237,7 +2292,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, this_ptr, &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()) @@ -2256,15 +2311,15 @@ impl Engine { } if let Some(path) = self - .eval_expr(scope, state, lib, this_ptr, &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; @@ -2288,11 +2343,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()) diff --git a/src/fn_native.rs b/src/fn_native.rs index e89cf334..040ff1f4 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -124,6 +124,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 { diff --git a/src/module.rs b/src/module.rs index caf3cb39..37d7c988 100644 --- a/src/module.rs +++ b/src/module.rs @@ -2,7 +2,7 @@ use crate::any::{Dynamic, Variant}; use crate::calc_fn_hash; -use crate::engine::{make_getter, make_setter, Engine, FN_IDX_GET, FN_IDX_SET}; +use crate::engine::{make_getter, make_setter, Engine, Imports, FN_IDX_GET, FN_IDX_SET}; use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync}; use crate::parser::{ FnAccess, @@ -10,13 +10,14 @@ use crate::parser::{ 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, iter::empty, @@ -24,6 +25,7 @@ use crate::stdlib::{ 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,6 +214,7 @@ 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. @@ -197,6 +246,7 @@ impl Module { fn_def.into(), ), ); + self.indexed = false; } /// Does a sub-module exist in the module? @@ -263,6 +313,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 +353,8 @@ impl Module { self.functions .insert(hash_fn, (name, access, params, func.into())); + self.indexed = false; + hash_fn } @@ -855,32 +908,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 +993,10 @@ impl Module { } } + if self.indexed { + return; + } + let mut variables = Vec::new(); let mut functions = Vec::new(); @@ -952,6 +1004,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 +1015,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 +1109,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 +1129,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 +1191,7 @@ mod file { Self { path: path.into(), extension: extension.into(), + cache: Default::default(), } } @@ -1173,12 +1236,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 +1318,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 +1328,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 7582aa6d..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,6 +122,7 @@ fn call_fn_with_constant_arguments( .engine .call_fn_raw( &mut Scope::new(), + &mut Imports::new(), &mut Default::default(), state.lib, fn_name, diff --git a/src/parser.rs b/src/parser.rs index 9a318849..275f0c00 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -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, @@ -202,6 +202,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 { @@ -214,9 +233,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. @@ -245,15 +266,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. @@ -261,32 +279,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 { @@ -1279,7 +1280,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())?, @@ -1469,7 +1470,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)))) } @@ -1477,7 +1478,6 @@ fn make_assignment_stmt<'a>( ScopeEntryType::Constant => { Err(PERR::AssignmentToConstant(name.clone()).into_err(*name_pos)) } - ScopeEntryType::Module => unreachable!(), } } // xxx[???] = rhs, xxx.??? = rhs @@ -1489,7 +1489,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)))) } @@ -1497,7 +1497,6 @@ fn make_assignment_stmt<'a>( ScopeEntryType::Constant => { Err(PERR::AssignmentToConstant(name.clone()).into_err(*name_pos)) } - ScopeEntryType::Module => unreachable!(), } } // expr[???] = rhs, expr.??? = rhs @@ -2017,13 +2016,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_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_len); Ok(Stmt::For(Box::new((name, expr, body)))) } @@ -2064,34 +2063,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 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!(), } } } @@ -2127,7 +2122,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))))) } @@ -2214,7 +2209,7 @@ 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_len = state.stack.len(); while !match_token(input, Token::RightBrace)? { // Parse statements inside the block @@ -2254,7 +2249,7 @@ fn parse_block( } } - state.truncate(prev_len); + state.stack.truncate(prev_len); Ok(Stmt::Block(Box::new((statements, settings.pos)))) } @@ -2389,7 +2384,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/scope.rs b/src/scope.rs index 39827ef5..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()) } @@ -421,8 +348,6 @@ impl<'a> Scope<'a> { Some((index, EntryType::Normal)) => { self.0.get_mut(index).unwrap().value = Dynamic::from(value) } - // modules cannot be modified - Some((_, EntryType::Module)) => unreachable!(), } } diff --git a/tests/modules.rs b/tests/modules.rs index 6f1deb92..d7f6ad7c 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -1,6 +1,7 @@ #![cfg(not(feature = "no_module"))] use rhai::{ - module_resolvers, Engine, EvalAltResult, Module, ParseError, ParseErrorType, Scope, INT, + module_resolvers::StaticModuleResolver, Engine, EvalAltResult, Module, ParseError, + ParseErrorType, Scope, INT, }; #[test] @@ -37,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 ); @@ -62,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); @@ -70,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)); @@ -155,12 +159,10 @@ 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#" @@ -190,44 +192,48 @@ fn test_module_from_ast() -> Result<(), Box> { export x as abc, foo, - hello, - extra as foobar; + 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" )); 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(()) } From 527d41d0e38e19497244bf096d589ff4db1973c6 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 28 Jun 2020 15:49:24 +0800 Subject: [PATCH 26/29] No modules in scope. --- RELEASES.md | 1 + src/engine.rs | 112 ++++++++++++++++++++++++++------------------------ src/module.rs | 16 ++++---- src/parser.rs | 10 +++-- 4 files changed, 75 insertions(+), 64 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 68eb31e4..494db8b5 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -13,6 +13,7 @@ Breaking changes * 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`. New features ------------ diff --git a/src/engine.rs b/src/engine.rs index 49c5650c..516f34fb 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -4,7 +4,7 @@ use crate::any::{Dynamic, Union, Variant}; use crate::calc_fn_hash; use crate::error::ParseErrorType; use crate::fn_native::{CallableFunction, Callback, FnCallArgs, FnPtr}; -use crate::module::{resolvers, Module, ModuleResolver}; +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}; @@ -393,6 +393,39 @@ 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, @@ -416,29 +449,16 @@ fn search_scope<'s, 'a>( } // 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 { - let offset = mods.len() - index.get(); - &mut mods.get_mut(offset).unwrap().1 - } else { - // Find the root module in the scope - let (id, root_pos) = modules.get(0); - - mods.iter_mut() - .rev() - .find(|(n, _)| n == id) - .map(|(_, m)| m) - .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)) @@ -1952,29 +1972,10 @@ impl Engine { let mut args: StaticVec<_> = arg_values.iter_mut().collect(); - let (id, root_pos) = modules.get(0); // First module - - let index = if state.always_search { - None - } else { - modules.index() - }; - - let module = 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 == id) - .map(|(_, m)| m) - .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) @@ -1990,7 +1991,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, }; @@ -2009,13 +2010,18 @@ impl Engine { 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)), + }, } } diff --git a/src/module.rs b/src/module.rs index 37d7c988..b6645d39 100644 --- a/src/module.rs +++ b/src/module.rs @@ -218,17 +218,19 @@ impl Module { } /// 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. @@ -839,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(), )) }) diff --git a/src/parser.rs b/src/parser.rs index 275f0c00..14c9d038 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2016,13 +2016,13 @@ fn parse_for( ensure_not_statement_expr(input, "a boolean")?; let expr = parse_expr(input, state, settings.level_up())?; - let prev_len = state.stack.len(); + 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.stack.truncate(prev_len); + state.stack.truncate(prev_stack_len); Ok(Stmt::For(Box::new((name, expr, body)))) } @@ -2209,7 +2209,8 @@ fn parse_block( settings.ensure_level_within_max_limit(state.max_expr_depth)?; let mut statements = StaticVec::new(); - let prev_len = state.stack.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 @@ -2249,7 +2250,8 @@ fn parse_block( } } - state.stack.truncate(prev_len); + state.stack.truncate(prev_stack_len); + state.modules.truncate(prev_mods_len); Ok(Stmt::Block(Box::new((statements, settings.pos)))) } From d6a08be2231b8f95809f689c1ad7867b6ac8ef61 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 29 Jun 2020 23:55:28 +0800 Subject: [PATCH 27/29] Refactor. --- README.md | 10 +- doc/src/SUMMARY.md | 32 ++--- doc/src/about/features.md | 2 +- doc/src/{about.md => about/index.md} | 2 +- doc/src/about/related.md | 2 + doc/src/engine/compile.md | 2 +- doc/src/{engine.md => engine/index.md} | 2 +- doc/src/engine/optimize/reoptimize.md | 6 +- doc/src/language/fn-namespaces.md | 141 +++++++++++++++++++++ doc/src/language/fn-ptr.md | 29 ++++- doc/src/{language.md => language/index.md} | 2 +- doc/src/language/method.md | 4 +- doc/src/language/modules/ast.md | 2 +- doc/src/language/modules/export.md | 4 +- doc/src/language/overload.md | 2 +- doc/src/links.md | 8 +- doc/src/{rust.md => rust/index.md} | 2 +- doc/src/safety.md | 1 - doc/src/safety/max-stmt-depth.md | 2 +- doc/src/start/features.md | 4 +- doc/src/{start.md => start/index.md} | 2 +- doc/src/start/install.md | 6 +- doc/src/start/playground.md | 10 ++ rhai_logo.png | Bin 0 -> 10280 bytes src/any.rs | 11 +- src/api.rs | 38 +++--- src/engine.rs | 127 +++++++++++-------- src/fn_func.rs | 4 +- src/fn_native.rs | 4 + src/lib.rs | 2 +- src/module.rs | 2 +- src/packages/array_basic.rs | 6 +- src/packages/string_more.rs | 15 ++- src/parser.rs | 36 +++--- src/token.rs | 24 +++- src/unsafe.rs | 10 -- tests/modules.rs | 2 +- 37 files changed, 386 insertions(+), 172 deletions(-) rename doc/src/{about.md => about/index.md} (86%) rename doc/src/{engine.md => engine/index.md} (88%) create mode 100644 doc/src/language/fn-namespaces.md rename doc/src/{language.md => language/index.md} (78%) rename doc/src/{rust.md => rust/index.md} (91%) delete mode 100644 doc/src/safety.md rename doc/src/{start.md => start/index.md} (81%) create mode 100644 doc/src/start/playground.md create mode 100644 rhai_logo.png diff --git a/README.md b/README.md index 7d55bdbe..952d9bf3 100644 --- a/README.md +++ b/README.md @@ -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). @@ -43,3 +45,9 @@ Documentation ------------- 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/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 6ea2fc76..f9491dd3 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -1,30 +1,31 @@ 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/index.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/index.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) @@ -42,7 +43,7 @@ The Rhai Scripting Language 4. [Printing Custom Types](rust/print-custom.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) @@ -70,9 +71,10 @@ 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) - 3. [Function Pointers](language/fn-ptr.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/index.md) 1. [Export Variables, Functions and Sub-Modules](language/modules/export.md) @@ -81,7 +83,7 @@ The Rhai Scripting Language 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/index.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) @@ -92,7 +94,7 @@ 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) +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) @@ -102,7 +104,7 @@ The Rhai Scripting Language 5. [Volatility Considerations](engine/optimize/volatility.md) 6. [Subtle Semantic Changes](engine/optimize/semantics.md) 3. [Eval Statement](language/eval.md) -8. [Appendix](appendix/index.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 75df3def..c012353a 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -8,7 +8,7 @@ Easy * Easy-to-use language similar to JavaScript+Rust with dynamic typing. -* Tight integration with native Rust [functions] and [types][custom types], including [getters/setters]({{rootUrl}}/rust/getters-setters.md), [methods][custom type] 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`]. 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/related.md b/doc/src/about/related.md index f91fdb64..defc29e4 100644 --- a/doc/src/about/related.md +++ b/doc/src/about/related.md @@ -11,6 +11,8 @@ Other online documentation resources for Rhai: * [`LIB.RS`](https://lib.rs/crates/rhai) - Rhai library info +* [Online Playground][playground] - Run scripts directly from editor + Other cool projects to check out: * [ChaiScript](http://chaiscript.com/) - A strong inspiration for Rhai. An embedded scripting language for C++ that I helped created many moons ago, now being led by my cousin. 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/reoptimize.md b/doc/src/engine/optimize/reoptimize.md index bd173be8..e8292e4c 100644 --- a/doc/src/engine/optimize/reoptimize.md +++ b/doc/src/engine/optimize/reoptimize.md @@ -4,13 +4,13 @@ Re-Optimize an AST {{#include ../../links.md}} 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`. +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` +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. +The final, optimized [`AST`] is then used for evaluations. ```rust // Compile master script to AST 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 index a9f2e64a..e4d9bc5d 100644 --- a/doc/src/language/fn-ptr.md +++ b/doc/src/language/fn-ptr.md @@ -54,7 +54,34 @@ 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)" +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! ``` 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/method.md b/doc/src/language/method.md index a9cdc1c7..bdce0ed6 100644 --- a/doc/src/language/method.md +++ b/doc/src/language/method.md @@ -3,9 +3,9 @@ Call Method as Function {{#include ../links.md}} -Property getters/setters and methods in a Rust custom type registered with the [`Engine`] can be called +Property [getters/setters] and [methods][custom types] in a Rust custom type registered with the [`Engine`] can be called just like a regular function. In fact, like Rust, property getters/setters and object methods -are registered as regular functions in Rhai that take a first `&mut` parameter. +are registered as regular [functions] in Rhai that take a first `&mut` parameter. 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). 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/overload.md b/doc/src/language/overload.md index b841d00a..bb291ca3 100644 --- a/doc/src/language/overload.md +++ b/doc/src/language/overload.md @@ -3,7 +3,7 @@ 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. diff --git a/doc/src/links.md b/doc/src/links.md index ae6ef01d..b7147a50 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -15,11 +15,13 @@ [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 @@ -38,6 +40,8 @@ [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 @@ -70,6 +74,8 @@ [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/index.md [module]: {{rootUrl}}/language/modules/index.md @@ -89,7 +95,7 @@ [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/index.md [`OptimizationLevel::Full`]: {{rootUrl}}/engine/optimize/optimize-levels.md 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/safety.md b/doc/src/safety.md deleted file mode 100644 index 1140c921..00000000 --- a/doc/src/safety.md +++ /dev/null @@ -1 +0,0 @@ -# Safety and Protection 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/features.md b/doc/src/start/features.md index c7cbd095..b4a415a9 100644 --- a/doc/src/start/features.md +++ b/doc/src/start/features.md @@ -14,7 +14,7 @@ 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`. | +| `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`. | @@ -24,7 +24,7 @@ more control over what a script can (or cannot) do. | `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. | +| `internals` | Expose internal data structures (e.g. [`AST`] nodes). Beware that Rhai internals are volatile and may change from version to version. | Example 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 b8ee54dc..a7dc944f 100644 --- a/doc/src/start/install.md +++ b/doc/src/start/install.md @@ -3,8 +3,10 @@ Install the Rhai Crate {{#include ../links.md}} -Install the Rhai crate from [`crates.io`](https:/crates.io/crates/rhai/), start by looking up the -latest version and 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] 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/rhai_logo.png b/rhai_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..af45aa7f65b4374f165852e3ad0391573e7c08ec GIT binary patch literal 10280 zcmb_?Ra6{Z)Ftj3G(d0(PUB8+cZc8vXdD`MCs>dKcXxMp3+@oyT^rZw@1J>?_nC)V zwyIX$bLGh1J3>WC8XbiQ1quoZT~2t;3T12U4UT>SjZILWW;1P z&+MDVdFn|n5?heH6BGp|FGpX@CofX@eoXoQO-*%&6GP%9`Q@Yf^MjA%m%=vXp`$ql z0be*3sAYtYB(-01a(je?#Ti5Tz%d@g%N`z;(I3zEeUIeV`2XXpGRA|Ecm;0f|Mu41 zH(igRx!BngZZ9`C_uZ#NJ_LdP8cN45O-)T@-k$G9(;d2wZ(gph9Q$VRd1!heeUj-h zXQvPrCMAlIdfhXuGE(lF*wb)2g|y8w+H|qR_n&JG_^HnMUzxS@lqy!dz1f+C6x>`T_}~uhFCwG z&kGf(2Ok8n{+b#l*Z;o(h<2LR@-#VjMU>(DbMxN=X{YIgHP`YrdPlw4WUl|7(B%?o z)wp^7&v*0)p?D)O^?fKdy(|cS@mX#@Q6j-OEhVuS=-s<=DW&G+M5>j{?<{s^@IDdW zBG4FSa6C9n^=1mYa5;lOgUW)4kXhLkg)4LZ$3W2hJ`CEG`V>k<(ECpG`nvn@XgnDH zfV6JKSqB5lpQ-dZ0Qw&b1}xlrPFl40<}4McjVhl$O`K?pR##0pQEijpGe0Ijo`1Qy zx!H2$%~`PiKYvh18`(s)!G^!F`|!SQqOjmtX**WexP#8~=IWh(XTI{cgN;wL9*72- zedwpvQaCYUQy#OZpyk!QD_|B7z` zY?sj{XQHZEv7OB(qjjdgzaA#54{uwW7#v~&lE_uU2TfVN;?HJ+cA0=N`A)U zi`n7-VxQQ*;dRO^hDvs`;e9zX(d~~}B9q+(YZYhw=Dvwl2{l{KH9{uv99LkVC^I|x zHKE~9^ycCg%QR=x^>lHf^|E=>?^MD4`Sy77JZLD3jKP`c^~L?6UQXY@QXLFg?4hBk z1bIf?en@C&)Zl{&&;~k2amUr-Nv>*H#6GgZ)OS0W9a` zxnS5{UbnLp^(~9~l@(g|;xIYE&)ITEJjt>v z+!y_mnND>BotWfYZ9c3mbRDogc?ZLEw>D#nGw90u3}C*uH_OjQ%zD_y+DSc|ydTB$ zufUj*~VX$Vffl1SzT73HTOx;6H@##P?T8mt#0Q8Rb3-qHHpuHjT;2 zWl%GdE~Zed@`}V3L~H1C9kEJ`MH{h1))YE0OlxP$x}ZA$xr*>H_M?t!UqqcrPCmXp zY|iA0BeJ+CMVP>7muiG+E!lAg36{fdKnts=b)MY9aGnUz)XrH(Z{>W3KgBGrbVx^r z?$SGqyC?sEDoI2zH&45OnC`||-&jLmAqkGf;@gF+3UL<~P6Ga73AVucSwSHob%;;8 zeWw_iXr>TaMZ#Gq=Msrc>cO{4J>?EXb`vYVkSnuJjEVe*5xx$C&RnEO%b{Qak4y9+ zfQ&~@T@gl@FexIn7oCzqsE{R?ikhizt0SA0ogLfK5~cGvY%6|B)aCdU*mNuxAO7zRqgb0wM@x;H87)cUqh}Z;h>=z z1dfi5PTDDdt4>|W5`=l1S?>z;8PMMkf6~tem?4l}EF!<$pCQiKjF6;$ z0SLS@KY8*2TfCS_Y1>%6cgn-HB$cYrwTSo_1+;I#>SdJvuh+x1PPzk>{Ow7#p2!WvW zpBd5mBUA81TnI$X6-wm~{3+mm$9n~oDw_47#`MyJ>})x0gKPo!D#zO+RqjOs#WYTA zk;I|NOo2hHaH^Q;X_do|k`sj6%e=u6KbQkWo1YFMHsO-2TzxfG=g5nDupEf( ze~hcqRE{)HN2&5cSN3EiJxmLSlVu$pT5q%cRNLqT9-=WP3ZxpwapYP+6^+l_BAX$# z(4@-XO^(QZ$dKl)L~a1$hX9nh1|Cf+r%y%wIni2Uh~9ZjY6g3?6Jg{h(Z3$g=~e1+ z9o81A9UFD{-mh5p1wg|NFh8u+TgDRZIIa~dXA6tz=n%3PjyrbIsr4eDM_C6>r-{0)ZCZrKU(kpAmJkjGBPVSJr!jZUUkIR5}2-SB<;^Ci3t=c z&yfuFd9?YIQ$r)hXfCEWr_~b~GH^Ol90Nu`|1hC>GKz?Zz+UDQDxF3PN)u>PN)k@YTEG|2+Lti_W`z=DHS|gft9xYZx4^z0Ivg`00ilLV`X3>W? zSyHt?XuWp8BqsgLN4#T$O<4ej)5rubd^t`33jHLhLgq$51qu9QhO#@qXO((&Jor&9 zK6K=Bxpp=F$wE1uee3W>r#ECt>K|lSyp|;H z2WD=V-eeROn$;`;tzHdyyCG7g-ZDF)$<`_Q6ISf?8EtOqFpL&a+5-1zPC=icgo^k++l zN+j9^4kl)saA^Q2sppq}nj4PfaC`?UuXUIB8}C=2I{M2fShn@9FEPCx3sP_&lr{D^ zBTrrA#+HrYwGY)&D)pOxNr5tx1hm*X^UnM(vQz3&MMOlBoz?-|$cJ;vB89T8O5GLF ztuoH8ergKk3keVpGFxjV-4{k#ty>>Pd;aXP|HEt;gUTe3Jh=T-=Ip`u=aUXo z)9Ue(i24gWb#_|$X}b+ycZtj-22E=l#@>lV+^snJXv8HMwY&unU7+>!E&8oWRqSQe zXj$82P9vHuA1N`r{)R$2_eAR>%tTIHVj_(nLnf8q=uU9=IC{8h9nx{Ng%q2VywdW82}*BngSFmaiqdoMu7TbUgv{5iG%{z2Ezk zaLPYB*mUYcW1JqBy5IM4-*A)#MVo9_)GU@-&5zsnQZ0u~pKnuneU9Ml#2MpLiUyi| z=^rh$Nt?*-`U?N(vAwtjQ034&YibUrRH>c>XMPq}Q&K#Ox|1Bg@FC$VmMEgZsy$@c zSqs&vc(dlB{Y7@@;-E!hPVd~A#kb)UdxQHVjwvL8@r9q--ZRUg`wf3Qzp_Qw;(M#J zBwC^{F^@eQF`6bM)*di)%vUcAuWn_?E#|2z!kwsX<+q^42$AB0@ZUk?U%>cZ1{vku zOy^ifd0l#mhglJS0t`S;Zg5K9wRWOn=xX=gV3%vQ*>@%jdlFJHj1*DQ78fW4WklzA z`ZErn;Wysm3P)B@>D^80*Z&6V)?35}BVtDC7Y;+h!AzcH6g!@mk5307gFux*dl)vA zl<{(nNugJzo~iq@y^ZP^YB_;O^n>NnPy&Iq=}a^NAwN0cyI;?Hf6uA>l-U(19PqRN za;L=antr5g9|f@_*xkxDj1U`O@zWiCb7!R=sha zF9^3{lxa)jef^bZ9%mF^hA{0SBtdOvH!WTzM$!>OiYI+r>bEKy*#Rd%%kukoW5c%w zOV0s74*|5Jo$##lKUV5XL3)}KY(ETOy)}0Uwit&J!k8rezz2!|7yrNFd*gv!giIXV zw#z+Q2VcRvNr=r3n{HhtINm{ma@nG>s6pIN(q_By30df&#=*UQ==^3(ee;&nt+E)j z7?qJr7EO}7R|IkZInpP_w>e%%tGY2^T%Wd!BjEwr2(>hQDd|BWHnME1 z0bz>zKWhsp6-aki^iEOkw2DTgng8D8Lh$q-k zM!07!CrpD4(LIoxV@M`Mm6J@OOzvB*H5>E6l2TJkAY>qEYNPRPR%y03r~y1> z{f&mC4eDiuo(c*Ip=Hef?JCm|5E>yTR^}PoD|eXV9l&u`2sYV5!au;f`lBC zN=D;wt54uD%;F|-UzIfnQ*pePsUUv#S ze8%ZUZ%ja#i#gqm@5_Be?#u4}K26wPgaa98$o|<`{5V=|u2x?baKFH%RnGcuZol5` z?@z(ay-5FXf?z^*F|>ejAAVQJ};_**hGYVYu0ac?HnAV!&Y!?^iugX}Q_`DbYA zH%q+HbvyD={coRmN=F;dxA&&LoR7ZL=gGJ_#7U`Y9#I%%hYQe_C-@xC7MhL-MeeE& z{r0(A(Ds0&it@KgTa8PGI9YzrK&LSwIgo@Nu&)EEmZ)!xwfih%nKBWQ-m{tDr58&_ zQ-KdyX-~UflHkH5D9P)H+(?!Ci&v+1y|Q?G#Kepn=kk^_pu3G>=WnfqANUlR*M~)k zw_8&^Cv~k*e6i|ektWy4b=rkUPs-$6&Sr=K`u-GS+CGwAcSI6eOh)wQ{NL%jzm}KK z@DYFCIU$Lx3j*RohU2)Z^LmJwv+a~K%FMRYO?#Mrd|9D62&lnrMulKtps%XMhVE|n z_FT!?#ZyY88a!X69upGc5wCUr?>7Q^S^T}CDE<@@h4y^|?c(m;&;8_Y360nTe`!lI zj5k)kXQS5tQJg>OYHm6itz1{#n~k*_rHdl^4IO(VTW^6d+oDTYx>2NByxCj5(t3&`D$8iU__?HLP)?WsVVi*2-Q)f+T=7qfCjJZ!Zt1jn)gF zn%DAM46Mb&ZgH#^N}>H7Xskm@vCP?awECQ^;FuFQ@@xDMdn4~gc|B$V1buyv`cHdg zG&GKdO;M^;tvIpJ0&A=&v9b?h4Z2fnU7NAPm9ds>b88Oxj5~bs3?tKjA|Z&qHdw89 z6%wurF?5dT;@RHpjqW79)Zi(PMfHbcdUve;;?|@t98HQzfW5xH7E5H%9H@M$w^?EV zP-oV}F|h4#NNtAeZ&yB10e)yOHk1H^CX*r?(o2_dL9kKDgQn}TJXqdBaJp$qEiUCp z`!VdHMJ-u4p~04TI`sg2Mq!~~s&Jew`O{};F?N3L`R)DD*saa#U*hK_3?t)X5)yYv zfTr{ohK;DYNHqh?Ly9UbhLw}WO8MW8nN=E$Dfe#`qQMB9X{QZx)*BleEV0Lb)3|J; z@>`UO>r#0EgUtCfA`oyX^JQh$Jqaff>gJ~9>!)s>G;`8tk%W$N=BFuX#Sugf%8{I3 zR`Hg?E+}B@q4~wPmJEE2o(ZMrlb8+&6Zr_LzwpRpt<^VOUp(U}k-q8ayeb^9%8E{b|WeX5xj zqdxR(P)CR0-rio16=&q87s7$O?NpY~KCNS#YDDg==wEYU?r#!GY)0-EYM&gwWwzgm z72)x^Fu+Td6ZqP5O-iCMx28H0;^cA;=b1>LWStnG&q;HMm;aNDy+7RmAen`64)lNN zz_OPK68>PnrN$%DB&}O1A*?2+w*2b2h4khd9K`0fgEWe%TIhdG8;ZxM^)vS-S2cf! zZ`hpxQW-!J6v0L@TS-wl1nq>ht)Rg%cvlsuCKAE$M{{Vc5Yf_k0ik*?;)bjGxlDVb zP*(7^XYv)2l>XyMj|7xeEJsll_^ z$X4K>SJ)3`XMcbH!)aAzOhI~<{r%9QJn#h1W?Ukpc7kLSJ~p>={9;Q61`!zqHnns> z0gVbeYzP*IYaKYZ70l_Tsz)TSSq#YJx>cq<-Vr+927RHY72$QEs9}UVWMZ$>LKig& zma>!5vz~DP-AvJ*#hd3!beu zOn$#<6_LPl{}nmOFD-RSnfx>APQHE@|B_->k>K!3^`#ZNJ{oXF1=d5&FJ`e%4Y6K? zv36O>iws2&)E`8{rjqo{g);Az!A(%l@SJMa_u8L#JXSd8}&82N0w5ZhU1VaY*7`QM-E z9$roe&b6U>U&+gT48NfKF6s_L%NN-&Xe3+x0O35H=+!X78gnDFJzDj&T_~oShKQ-C z!880JO1Xi@s)T;d;crRXiE<^LGQI5O)Ii1@Q16$H@+QF4jp)#>HawFiu&OfLhMT+^ z=+{g&rXo$Pv7E{>$PJrGx^+(hpZKu5zr*a*nCE-xP|wB$K3){#+O9f?m9+|=orJM% za6{q4GPRl(K&tqr7F{6Jby=W-v;j%?UXm zeoDfe<|fFyFg~wro$xp5JtL($}E4=(j9(?t7KITSGldFT+52X;p z%TV!Qj_c7S7mo36K1hOl7EJy-|$KRfX{ zZT#1}h%LrY<*)DAfBeN|(r>C5mvg>ni|T#3`HvHc02I2}Hw;iWSTotLlRd9`zkW{< zkA&pQk@|Qz+^97zzeAFk-$KTHw)t+)a3hMapIQ`G_>x?xm3#T1$d;Wu>?j0J+_5W;FW4p+)}RZCdHLfA#lldD)38b!LE@T|(*B z`V}@t4}TrGTt+0!U1ss1~&%mQlc4KiJ#jz`Nt5}{d+pHVxR-x`rANz;nV!w3;jEp?DU^(=@HZSIT@S2Fp$-7QU?c>ciR&8J?tE0#jXcV^c5-suN>$oeIx(2kwIxh?cU zanj5-R-qzefe_UqBoE}>G|&)`Q?@XWQdO#_81?HsAR<4Q%~Fm^kVhtJ<4eMHq0=Y5 z`Ek)W+Ak4KhfY)x7KDC8fFw?a&=JdtG@7ur;KUR>>{fJ|PI=&D1tYp#+RDH4AW&~` z&lc34g^{@14d0R7(iZb;Ae0>m>G@IAV7<4oOg29jlccgrss0y2z5*I4H0v-e*Ym5o z?4Ul6&qTvqI?h#;Jcszvv>Yj{V-^#Id{Hg(g;;#v`jfA*^4qDFS*4DHMI`b!5bV)= zFOQOnDs#c=AjxYwN0d;=i!+7iUmz@Eommb9PWRXfM4WDOwK?n>UjRALA74%%&UDHA zgk>4FoaJ^W@34Z`Gs!GnIJ)UV)(x?}Rr&a@QRCPb@L_xBC;g44kK*K36ePreJoy`AL zbjt!r0JpG|v-r+hQGE4u9uBP3LjIZ*tgVIos&e=k>VC zaeRGr`mev>^_J?c=Q0jh$K-^?l79_ASVqMipYQn2E2&)3 z2jZVTo&^J$2O1$^@{|2j&{wLX#+h+>ld$Hb$fUBr){W$wDPhwAd9nM2HGTGS!O0s4 zl^f7WZ_8GCTD`tp9t(I%-49>bSC~1Vd1kbZU(a#`yJIzO( zfwQ5iS{*jY2T{QmD`yfavpA-^x86^xzHJJLRh1~ zCr5Ur7+Zbjz19AB%RBd`N-oKEgYcQ$pa@}nH$`%iG6WfgyZ(z{;3Mq;TGj00^l#Jf9^XGH zz2-_JH6pmYSDL7)_1c_9c(;cgolqGq{A?OLrMlK)yC<8F9FKM4b)1hxbUKcli+RtmbQD`2v4MgBWt!Y8v~5& ze7brgWmyMJ7uvv&=a^sKWvU_+1ixM!r;RmJURyd-`>k0H5nrAUb|)JQRGyKe5rfg( z-e1ctt-mp8|2D<@;RNq=R7TC15=K|C1&FMhbt3|p4WT6uGmoPTxi47}um13=isp;> zO1L@Kpl1sUPeCm<21mk;u%bbU zuE3k{7D9!f!`nl1vVXDC5o3`82?_nAEVW{F3!#0p2k%M14enpuV%(#1X~ zAhW0eB`4$fEuIgv{qe@$qTHE~X%8Hhq{JVpBk_h6qgIO2skp7v_$7=a?GM zC6IzS7)eWdhU0BK1WER@g-L(Ymp%}VyyZ-+9P>@1&5~Rfy+ONMq8-psi;|4PB6{_~ z7lOW+mbl~`QiiP+S6rw|=Bh|_StZwU#BIBi2u zNeJ(K;f3iey_d{tG}IT0nh9E5;alfwN?_3k`(17I?(HWV(W(u>YM7f-I3wUy5<15Q zX$tYFBvG9~5(}F^?Mbwjrd7JfahG8(;4XLg38sM@5=&Awso^JhU)+t8{KVXHN`C zU5F$DsIheXAK1n8SI(Oa4JW327vBPYSH5uvO@@KF-CIcw;bd(p44wFX(?#ACe(tRt zZ>^bAft$wD#ifDV6}SaauS8II_V`WLg`mrr&dH3WL-^Gwif<mXe!|F!(?2W|3oH8c%J%JAgRrZUS zHmf#&K&A}FPCQEg$CUSFCtcXZ)c}q%?x{gwMttJPXEg%}z<=yv6jOVLToyzXf(k#V zgY5y{X0-qRZSFLtwN%~1f`F+Wm8g84k!zvg8%+q17LpP zZd$~Vn47(_-nG%X&k`(s7w+(!{PwInIQ8f@yRF3&JQmxbc;p6%46h@zC4JXS4 z1{OATSH{h^57KQ$gY=hvOcC5=$|(O(&uZp!8xu_?z{vI=QYKg-uiFw3)+|Il1EHEi z$H&LL4e`qstl{9{DFg*G?z~=?tRm=S(JK5`joH3cFA)GMRyCv5XkV}TF+)6SfYUk9 z={lMg!w3J#+kP>XpORo4`u~3xwEbT)xjKYL|2WT@kK&8;ONhD;N>)-yqDss-=zjtI Cm=(eR literal 0 HcmV?d00001 diff --git a/src/any.rs b/src/any.rs index d99712e1..bc148956 100644 --- a/src/any.rs +++ b/src/any.rs @@ -545,6 +545,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()), } } @@ -561,15 +562,7 @@ impl Dynamic { pub(crate) fn take_immutable_string(self) -> Result { match self.0 { Union::Str(s) => Ok(s), - _ => Err(self.type_name()), - } - } - - /// Cast the `Dynamic` as a `FnPtr` and return the function name. - /// Returns the name of the actual type if the cast fails. - pub(crate) fn as_fn_name(&self) -> Result<&str, &'static str> { - match &self.0 { - Union::FnPtr(f) => Ok(f.fn_name()), + Union::FnPtr(f) => Ok(f.take_fn_name()), _ => Err(self.type_name()), } } diff --git a/src/api.rs b/src/api.rs index 66050214..2f0e3e19 100644 --- a/src/api.rs +++ b/src/api.rs @@ -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 /// @@ -986,7 +986,7 @@ impl Engine { }); } - /// Evaluate an `AST` with own scope. + /// Evaluate an [`AST`] with own scope. pub(crate) fn eval_ast_with_scope_raw<'a>( &self, scope: &mut Scope, @@ -1052,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, @@ -1076,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 @@ -1133,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 /// @@ -1179,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 /// @@ -1220,15 +1220,15 @@ impl Engine { ) } - /// 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 516f34fb..65c91735 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -8,10 +8,10 @@ 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"))] @@ -75,6 +75,7 @@ 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 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"; @@ -82,7 +83,6 @@ 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$"; -pub const FN_FN_PTR: &str = "Fn"; /// A type specifying the method of chaining. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] @@ -729,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); } @@ -1020,7 +1027,7 @@ impl Engine { )), // Fn - FN_FN_PTR if args.len() == 1 && !self.has_override(lib, hashes) => { + 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(), @@ -1133,7 +1140,7 @@ impl Engine { state, lib, this_ptr, obj_ptr, expr, idx_values, next_chain, level, new_val, ) - .map_err(|err| EvalAltResult::new_position(err, *pos)) + .map_err(|err| err.new_position(*pos)) } // xxx[rhs] = new_val _ if new_val.is_some() => { @@ -1162,9 +1169,9 @@ impl Engine { } // Indexed value is a reference - update directly Ok(ref mut obj_ptr) => { - obj_ptr.set_value(new_val.unwrap()).map_err(|err| { - EvalAltResult::new_position(err, rhs.position()) - })?; + 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 @@ -1205,11 +1212,12 @@ impl Engine { 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 name == KEYWORD_FN_PTR_CALL && obj.is::() { + if fn_name == KEYWORD_FN_PTR_CALL && obj.is::() { // Redirect function name - let fn_name = obj.as_fn_name().unwrap(); + fn_name = obj.as_str().unwrap(); // Recalculate hash let hash = calc_fn_hash(empty(), fn_name, idx.len(), empty()); // Arguments are passed as-is @@ -1222,25 +1230,20 @@ impl Engine { def_val, level, ) } else { - let mut fn_name = name.clone(); - let mut redirected = None; + 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(name.as_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().as_str().into(); + fn_name = redirected.as_ref().unwrap(); // Recalculate the hash based on the new function name - hash = calc_fn_hash( - empty(), - fn_name.as_ref(), - idx.len(), - empty(), - ); + hash = + calc_fn_hash(empty(), fn_name, idx.len(), empty()); } } }; @@ -1249,14 +1252,13 @@ impl Engine { let mut arg_values = once(obj).chain(idx.iter_mut()).collect::>(); let args = arg_values.as_mut(); - let fn_name = fn_name.as_ref(); self.exec_fn_call( state, lib, fn_name, *native, hash, args, is_ref, true, def_val, level, ) } - .map_err(|err| EvalAltResult::new_position(err, *pos))? + .map_err(|err| err.new_position(*pos))? }; // Feed the changed temp value back @@ -1277,7 +1279,7 @@ impl Engine { self.get_indexed_mut(state, lib, target, index, *pos, true, level)?; val.set_value(new_val.unwrap()) - .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; + .map_err(|err| err.new_position(rhs.position()))?; Ok((Default::default(), true)) } // {xxx:map}.id @@ -1297,7 +1299,7 @@ impl Engine { state, lib, setter, true, 0, &mut args, is_ref, true, None, level, ) .map(|(v, _)| (v, true)) - .map_err(|err| EvalAltResult::new_position(err, *pos)) + .map_err(|err| err.new_position(*pos)) } // xxx.id Expr::Property(x) => { @@ -1307,7 +1309,7 @@ impl Engine { state, lib, getter, true, 0, &mut args, is_ref, true, None, level, ) .map(|(v, _)| (v, false)) - .map_err(|err| EvalAltResult::new_position(err, *pos)) + .map_err(|err| err.new_position(*pos)) } // {xxx:map}.prop[expr] | {xxx:map}.prop.expr Expr::Index(x) | Expr::Dot(x) if target.is::() => { @@ -1325,7 +1327,7 @@ impl Engine { state, lib, this_ptr, &mut val, expr, idx_values, next_chain, level, new_val, ) - .map_err(|err| EvalAltResult::new_position(err, *pos)) + .map_err(|err| err.new_position(*pos)) } // xxx.prop[expr] | xxx.prop.expr Expr::Index(x) | Expr::Dot(x) => { @@ -1338,7 +1340,7 @@ impl Engine { self.exec_fn_call( state, lib, getter, true, 0, args, is_ref, true, None, level, ) - .map_err(|err| EvalAltResult::new_position(err, *pos))? + .map_err(|err| err.new_position(*pos))? } else { unreachable!(); }; @@ -1350,7 +1352,7 @@ impl Engine { state, lib, this_ptr, target, expr, idx_values, next_chain, level, new_val, ) - .map_err(|err| EvalAltResult::new_position(err, *pos))?; + .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 { @@ -1364,7 +1366,7 @@ impl Engine { .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)), })?; } } @@ -1413,7 +1415,7 @@ impl Engine { let (var_name, var_pos) = &x.0; self.inc_operations(state) - .map_err(|err| EvalAltResult::new_position(err, *var_pos))?; + .map_err(|err| err.new_position(*var_pos))?; let (target, _, typ, pos) = search_scope(scope, mods, state, this_ptr, dot_lhs)?; @@ -1433,7 +1435,7 @@ impl Engine { 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() => { @@ -1449,7 +1451,7 @@ impl Engine { 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)) } } } @@ -1472,7 +1474,7 @@ impl Engine { 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() => { @@ -1627,7 +1629,7 @@ impl Engine { 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, mods, state, lib, this_ptr, lhs, level)?; let rhs_value = self.eval_expr(scope, mods, state, lib, this_ptr, rhs, level)?; @@ -1655,7 +1657,7 @@ impl Engine { &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()); } @@ -1694,7 +1696,7 @@ impl Engine { 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, mods, state, lib, this_ptr, x.as_ref(), level), @@ -1728,7 +1730,7 @@ impl Engine { 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 @@ -1769,7 +1771,7 @@ impl Engine { 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()) } @@ -1795,7 +1797,7 @@ impl Engine { ]; 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 { @@ -1859,8 +1861,8 @@ impl Engine { let ((name, native, pos), _, hash, args_expr, def_val) = x.as_ref(); let def_val = def_val.as_ref(); - // Handle Fn - if name == FN_FN_PTR && args_expr.len() == 1 { + // Handle Fn() + if name == KEYWORD_FN_PTR && args_expr.len() == 1 { let hash_fn = calc_fn_hash(empty(), name, 1, once(TypeId::of::())); @@ -1871,17 +1873,27 @@ impl Engine { self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; return arg_value .take_immutable_string() - .map(|s| FnPtr::from(s).into()) .map_err(|type_name| { Box::new(EvalAltResult::ErrorMismatchOutputType( type_name.into(), - Position::none(), + 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 + // Handle eval() if name == KEYWORD_EVAL && args_expr.len() == 1 { let hash_fn = calc_fn_hash(empty(), name, 1, once(TypeId::of::())); @@ -1894,7 +1906,7 @@ impl Engine { self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; let result = self .eval_script_expr(scope, mods, state, lib, &script) - .map_err(|err| EvalAltResult::new_position(err, expr.position())); + .map_err(|err| err.new_position(expr.position())); if scope.len() != prev_len { // IMPORTANT! If the eval defines new variables in the current scope, @@ -1932,7 +1944,7 @@ impl Engine { search_scope(scope, mods, state, this_ptr, lhs)?; self.inc_operations(state) - .map_err(|err| EvalAltResult::new_position(err, pos))?; + .map_err(|err| err.new_position(pos))?; args = once(target).chain(arg_values.iter_mut()).collect(); @@ -1957,7 +1969,7 @@ impl Engine { 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 @@ -1979,7 +1991,7 @@ impl Engine { 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, @@ -2005,7 +2017,7 @@ impl Engine { self.call_script_fn( &mut scope, &mut mods, state, lib, &mut None, name, fn_def, args, level, ) - .map_err(|err| EvalAltResult::new_position(err, *pos)) + .map_err(|err| err.new_position(*pos)) } Ok(f) => { f.get_native_fn()(self, args.as_mut()).map_err(|err| err.new_position(*pos)) @@ -2071,6 +2083,7 @@ impl Engine { }; self.check_data_size(result) + .map_err(|err| err.new_position(expr.position())) } /// Evaluate a statement @@ -2085,7 +2098,7 @@ impl Engine { 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 @@ -2200,7 +2213,7 @@ 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, mods, state, lib, this_ptr, stmt, level) { Ok(_) => (), @@ -2368,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/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 040ff1f4..e41ae7db 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -67,6 +67,10 @@ impl FnPtr { 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 { diff --git a/src/lib.rs b/src/lib.rs index af902d9b..4c37149c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,7 @@ //! | `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. diff --git a/src/module.rs b/src/module.rs index 7eeb4c8e..043d14dc 100644 --- a/src/module.rs +++ b/src/module.rs @@ -901,7 +901,7 @@ impl Module { .map(|f| f.get_shared_fn_def()) } - /// Create a new `Module` by evaluating an `AST`. + /// Create a new `Module` by evaluating an [`AST`]. /// /// # Examples /// diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index d492f52f..2b9da1f5 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -42,12 +42,10 @@ fn pad(engine: &Engine, args: &mut [&mut Dynamic]) -> FuncRe } if len >= 0 { - let item = args[2].downcast_ref::().unwrap().clone(); + let item = args[2].clone(); let list = args[0].downcast_mut::().unwrap(); - while list.len() < len as usize { - push(list, item.clone())?; - } + list.resize(len as usize, item); } Ok(()) } diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index a472c2dd..2037c215 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -245,16 +245,21 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str 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 orig_len < len as usize { + let p = s.make_mut(); + + for _ in 0..(len as usize - orig_len) { + p.push(ch); + } } - if engine.max_string_size > 0 && copy.len() > engine.max_string_size { + if engine.max_string_size > 0 && s.len() > engine.max_string_size { Err(Box::new(EvalAltResult::ErrorDataTooLarge( "Length of string".to_string(), engine.max_string_size, - copy.len(), + s.len(), Position::none(), ))) } else { diff --git a/src/parser.rs b/src/parser.rs index 14c9d038..69289de0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -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,7 +59,7 @@ pub struct AST( ); impl AST { - /// Create a new `AST`. + /// Create a new [`AST`]. pub fn new(statements: Vec, lib: Module) -> Self { Self(statements, lib) } @@ -95,16 +95,16 @@ impl AST { &self.1 } - /// Merge two `AST` into one. Both `AST`'s are untouched and a new, merged, version + /// 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 /// @@ -157,13 +157,13 @@ impl AST { Self::new(ast, functions) } - /// Clear all function definitions in the `AST`. + /// 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. + /// Clear all statements in the [`AST`], leaving only function definitions. #[cfg(not(feature = "no_function"))] pub fn retain_functions(&mut self) { self.0 = vec![]; @@ -802,22 +802,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); @@ -1259,8 +1259,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 { diff --git a/src/token.rs b/src/token.rs index 5cdc1771..4b19ab49 100644 --- a/src/token.rs +++ b/src/token.rs @@ -453,6 +453,22 @@ pub trait InputStream { 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, @@ -783,13 +799,9 @@ fn get_next_token_inner( } } - 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 is_valid_identifier = is_valid_identifier(result.iter().cloned()); - let identifier: String = result.iter().collect(); + let identifier: String = result.into_iter().collect(); if !is_valid_identifier { return Some(( 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/modules.rs b/tests/modules.rs index d7f6ad7c..b110fa8e 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -235,7 +235,7 @@ fn test_module_from_ast() -> Result<(), Box> { *engine .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(()) From 52ef223f83e6562434cb6f7915952545a9113cb7 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 30 Jun 2020 18:34:32 +0800 Subject: [PATCH 28/29] Fix pad functions. --- src/packages/array_basic.rs | 6 ++++-- src/packages/string_more.rs | 36 +++++++++++++++++++----------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index 2b9da1f5..bf06ed47 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -41,11 +41,13 @@ fn pad(engine: &Engine, args: &mut [&mut Dynamic]) -> FuncRe } } - if len >= 0 { + if len > 0 { let item = args[2].clone(); let list = args[0].downcast_mut::().unwrap(); - list.resize(len as usize, item); + if len as usize > list.len() { + list.resize(len as usize, item); + } } Ok(()) } diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 2037c215..7bc10012 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -242,29 +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 orig_len = s.chars().count(); + let orig_len = s.chars().count(); - if orig_len < len as usize { - let p = s.make_mut(); + if len as usize > orig_len { + let p = s.make_mut(); - for _ in 0..(len as usize - orig_len) { - p.push(ch); + 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 && s.len() > engine.max_string_size { - Err(Box::new(EvalAltResult::ErrorDataTooLarge( - "Length of string".to_string(), - engine.max_string_size, - s.len(), - Position::none(), - ))) - } else { - Ok(()) - } + Ok(()) }, ); lib.set_fn_3_mut( From e614790897346c368dd3e38f25c7a196b3d7fb28 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 30 Jun 2020 18:34:58 +0800 Subject: [PATCH 29/29] Enable function pointers even under no_function. --- RELEASES.md | 2 + doc/src/appendix/keywords.md | 4 +- doc/src/language/keywords.md | 1 + doc/src/language/values-and-types.md | 2 +- examples/repl.rs | 3 +- src/any.rs | 11 --- src/lib.rs | 5 +- src/module.rs | 45 ++++++++++- src/parser.rs | 112 +++++++++++++++++++++++++-- tests/modules.rs | 1 + 10 files changed, 162 insertions(+), 24 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 494db8b5..6f38bb72 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -14,6 +14,7 @@ Breaking changes 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 ------------ @@ -21,6 +22,7 @@ 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 ------------ diff --git a/doc/src/appendix/keywords.md b/doc/src/appendix/keywords.md index 6e13178f..660a605c 100644 --- a/doc/src/appendix/keywords.md +++ b/doc/src/appendix/keywords.md @@ -24,8 +24,8 @@ Keywords List | `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] | [`no_function`] | -| `call` | Call a [function pointer] | [`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 | | diff --git a/doc/src/language/keywords.md b/doc/src/language/keywords.md index 82eddce4..836e9b0c 100644 --- a/doc/src/language/keywords.md +++ b/doc/src/language/keywords.md @@ -15,6 +15,7 @@ The following are reserved keywords in Rhai: | `return` | Return values | | | `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 feature is enabled. diff --git a/doc/src/language/values-and-types.md b/doc/src/language/values-and-types.md index 5a5ae2e0..70c276a9 100644 --- a/doc/src/language/values-and-types.md +++ b/doc/src/language/values-and-types.md @@ -15,7 +15,7 @@ The following primitive types are supported natively: | **[`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]** (disabled with [`no_function`]) | _None_ | `Fn` | `"Fn(foo)"` | +| **[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. | diff --git a/examples/repl.rs b/examples/repl.rs index 81f42158..cf255986 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -166,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/src/any.rs b/src/any.rs index bc148956..7e8b8072 100644 --- a/src/any.rs +++ b/src/any.rs @@ -137,7 +137,6 @@ pub enum Union { Array(Box), #[cfg(not(feature = "no_object"))] Map(Box), - #[cfg(not(feature = "no_function"))] FnPtr(FnPtr), Variant(Box>), } @@ -175,7 +174,6 @@ impl Dynamic { Union::Array(_) => TypeId::of::(), #[cfg(not(feature = "no_object"))] Union::Map(_) => TypeId::of::(), - #[cfg(not(feature = "no_function"))] Union::FnPtr(_) => TypeId::of::(), Union::Variant(value) => (***value).type_id(), } @@ -195,7 +193,6 @@ impl Dynamic { Union::Array(_) => "array", #[cfg(not(feature = "no_object"))] Union::Map(_) => "map", - #[cfg(not(feature = "no_function"))] Union::FnPtr(_) => "Fn", #[cfg(not(feature = "no_std"))] @@ -220,7 +217,6 @@ impl fmt::Display for Dynamic { Union::Array(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_object"))] Union::Map(value) => write!(f, "#{:?}", value), - #[cfg(not(feature = "no_function"))] Union::FnPtr(value) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_std"))] @@ -245,7 +241,6 @@ impl fmt::Debug for Dynamic { Union::Array(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_object"))] Union::Map(value) => write!(f, "#{:?}", value), - #[cfg(not(feature = "no_function"))] Union::FnPtr(value) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_std"))] @@ -270,7 +265,6 @@ impl Clone for Dynamic { Union::Array(ref value) => Self(Union::Array(value.clone())), #[cfg(not(feature = "no_object"))] Union::Map(ref value) => Self(Union::Map(value.clone())), - #[cfg(not(feature = "no_function"))] Union::FnPtr(ref value) => Self(Union::FnPtr(value.clone())), Union::Variant(ref value) => (***value).clone_into_dynamic(), } @@ -400,7 +394,6 @@ impl Dynamic { Union::Array(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), #[cfg(not(feature = "no_object"))] Union::Map(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), - #[cfg(not(feature = "no_function"))] Union::FnPtr(value) => unsafe_try_cast(value), Union::Variant(value) => (*value).as_box_any().downcast().map(|x| *x).ok(), } @@ -444,7 +437,6 @@ impl Dynamic { Union::Array(value) => *unsafe_cast_box::<_, T>(value).unwrap(), #[cfg(not(feature = "no_object"))] Union::Map(value) => *unsafe_cast_box::<_, T>(value).unwrap(), - #[cfg(not(feature = "no_function"))] Union::FnPtr(value) => unsafe_try_cast(value).unwrap(), Union::Variant(value) => (*value).as_box_any().downcast().map(|x| *x).unwrap(), } @@ -471,7 +463,6 @@ impl Dynamic { Union::Array(value) => ::downcast_ref::(value.as_ref()), #[cfg(not(feature = "no_object"))] Union::Map(value) => ::downcast_ref::(value.as_ref()), - #[cfg(not(feature = "no_function"))] Union::FnPtr(value) => ::downcast_ref::(value), Union::Variant(value) => value.as_ref().as_ref().as_any().downcast_ref::(), } @@ -497,7 +488,6 @@ impl Dynamic { Union::Array(value) => ::downcast_mut::(value.as_mut()), #[cfg(not(feature = "no_object"))] Union::Map(value) => ::downcast_mut::(value.as_mut()), - #[cfg(not(feature = "no_function"))] Union::FnPtr(value) => ::downcast_mut::(value), Union::Variant(value) => value.as_mut().as_mut_any().downcast_mut::(), } @@ -626,7 +616,6 @@ impl, T: Variant + Clone> From> for Dynam ))) } } -#[cfg(not(feature = "no_function"))] impl From for Dynamic { fn from(value: FnPtr) -> Self { Self(Union::FnPtr(value)) diff --git a/src/lib.rs b/src/lib.rs index 4c37149c..aa205dec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,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; @@ -135,7 +138,7 @@ pub use token::{get_next_token, parse_string_literal, InputStream, Token, Tokeni #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] -pub use parser::{Expr, FnAccess, ReturnType, ScriptFnDef, Stmt}; +pub use parser::{Expr, ReturnType, ScriptFnDef, Stmt}; #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] diff --git a/src/module.rs b/src/module.rs index 043d14dc..3cdeb711 100644 --- a/src/module.rs +++ b/src/module.rs @@ -859,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. diff --git a/src/parser.rs b/src/parser.rs index 69289de0..906d99de 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -116,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' /// @@ -138,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()) { @@ -152,11 +218,39 @@ impl AST { }; let mut functions = functions.clone(); - functions.merge(&other.1); + functions.merge_filtered(&other.1, filter); Self::new(ast, functions) } + /// 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) { @@ -164,8 +258,7 @@ impl AST { } /// Clear all statements in the [`AST`], leaving only function definitions. - #[cfg(not(feature = "no_function"))] - pub fn retain_functions(&mut self) { + pub fn clear_statements(&mut self) { self.0 = vec![]; } } @@ -187,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 { diff --git a/tests/modules.rs b/tests/modules.rs index b110fa8e..0e1eb3ba 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -250,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