diff --git a/Cargo.toml b/Cargo.toml index 4fc4877a..db72ea25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ [package] name = "rhai" -version = "0.19.5" +version = "0.19.6" edition = "2018" authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung", "jhwgh1968"] description = "Embedded scripting for Rust" diff --git a/RELEASES.md b/RELEASES.md index 1e200e97..4913a137 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,6 +1,17 @@ Rhai Release Notes ================== +Version 0.19.6 +============== + +This version adds the `switch` statement. + +New features +------------ + +* `switch` statement. + + Version 0.19.5 ============== @@ -209,7 +220,7 @@ Bug fixes --------- * Fixes bug that prevents calling functions in closures. -* Fixes bug that erroneously consumes the first argument to a module-qualified function call. +* Fixes bug that erroneously consumes the first argument to a namespace-qualified function call. New features ------------ @@ -299,7 +310,7 @@ New features * The boolean `^` (XOR) operator is added. * `FnPtr` is exposed as the function pointer type. * `rhai::module_resolvers::ModuleResolversCollection` added to try a list of module resolvers. -* It is now possible to mutate the first argument of a module-qualified function call when the argument is a simple variable (but not a module constant). +* It is now possible to mutate the first argument of a namespace-qualified function call when the argument is a simple variable (but not a module constant). * Many configuration/setting API's now returns `&mut Self` so that the calls can be chained. * `String` parameters in functions are supported (but inefficiently). diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 3d7a3ca9..f0ee7b6a 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -77,13 +77,14 @@ The Rhai Scripting Language 7. [Logic Operators](language/logic.md) 8. [Other Operators](language/other-op.md) 9. [If Statement](language/if.md) - 10. [While Loop](language/while.md) - 11. [Loop Statement](language/loop.md) - 12. [For Loop](language/for.md) - 13. [Return Values](language/return.md) - 14. [Throw Exception on Error](language/throw.md) - 15. [Catch Exceptions](language/try-catch.md) - 16. [Functions](language/functions.md) + 10. [Switch Expression](language/switch.md) + 11. [While Loop](language/while.md) + 12. [Loop Statement](language/loop.md) + 13. [For Loop](language/for.md) + 14. [Return Values](language/return.md) + 15. [Throw Exception on Error](language/throw.md) + 16. [Catch Exceptions](language/try-catch.md) + 17. [Functions](language/functions.md) 1. [Call Method as Function](language/method.md) 2. [Overloading](language/overload.md) 3. [Namespaces](language/fn-namespaces.md) @@ -91,11 +92,11 @@ The Rhai Scripting Language 5. [Currying](language/fn-curry.md) 6. [Anonymous Functions](language/fn-anon.md) 7. [Closures](language/fn-closure.md) - 17. [Print and Debug](language/print-debug.md) - 18. [Modules](language/modules/index.md) + 18. [Print and Debug](language/print-debug.md) + 19. [Modules](language/modules/index.md) 1. [Export Variables, Functions and Sub-Modules](language/modules/export.md) 2. [Import Modules](language/modules/import.md) - 19. [Eval Statement](language/eval.md) + 20. [Eval Statement](language/eval.md) 6. [Safety and Protection](safety/index.md) 1. [Checked Arithmetic](safety/checked.md) 2. [Sand-Boxing](safety/sandbox.md) diff --git a/doc/src/appendix/keywords.md b/doc/src/appendix/keywords.md index acf603f8..ad8d3e26 100644 --- a/doc/src/appendix/keywords.md +++ b/doc/src/appendix/keywords.md @@ -13,6 +13,7 @@ Keywords List | `is_shared` | is a value shared? | [`no_closure`] | yes | no | | `if` | if statement | | no | | | `else` | else block of if statement | | no | | +| `switch` | matching | | no | | | `while` | while loop | | no | | | `loop` | infinite loop | | no | | | `for` | for loop | | no | | @@ -52,7 +53,6 @@ Reserved Keywords | `then` | control flow | | `goto` | control flow | | `exit` | control flow | -| `switch` | matching | | `match` | matching | | `case` | matching | | `public` | function/field access | diff --git a/doc/src/appendix/operators.md b/doc/src/appendix/operators.md index ff9b7fed..0ca26be5 100644 --- a/doc/src/appendix/operators.md +++ b/doc/src/appendix/operators.md @@ -39,6 +39,7 @@ Symbols and Patterns | Symbol | Name | Description | | ---------------------------------- | :------------------: | ------------------------------------- | +| `_` | underscore | default `switch` case | | `;` | semicolon | statement separator | | `,` | comma | list separator | | `:` | colon | [object map] property value separator | @@ -52,6 +53,7 @@ Symbols and Patterns | \| .. \| | pipes | closure | | `[` .. `]` | brackets | [array] literal | | `!` | bang | function call in calling scope | +| `=>` | double arrow | `switch` expression case separator | | `//` | comment | line comment | | `/*` .. `*/` | comment | block comment | | `(*` .. `*)` | comment | _reserved_ | @@ -64,7 +66,6 @@ Symbols and Patterns | `#` | hash | _reserved_ | | `@` | at | _reserved_ | | `$` | dollar | _reserved_ | -| `=>` | double arrow | _reserved_ | | `->` | arrow | _reserved_ | | `<-` | left arrow | _reserved_ | | `===` | strict equals to | _reserved_ | diff --git a/doc/src/context.json b/doc/src/context.json index 597eb0df..7bb1a3ce 100644 --- a/doc/src/context.json +++ b/doc/src/context.json @@ -1,5 +1,5 @@ { - "version": "0.19.5", + "version": "0.19.6", "repoHome": "https://github.com/jonathandturner/rhai/blob/master", "repoTree": "https://github.com/jonathandturner/rhai/tree/master", "rootUrl": "", diff --git a/doc/src/engine/dsl.md b/doc/src/engine/dsl.md index 593a7eb0..efb9ac81 100644 --- a/doc/src/engine/dsl.md +++ b/doc/src/engine/dsl.md @@ -49,7 +49,7 @@ For example: let animal = "rabbit"; let food = "carrot"; -animal eats food // custom operator - 'eats' +animal eats food // custom operator 'eats' eats(animal, food) // <- the above really de-sugars to this ``` @@ -61,7 +61,7 @@ nevertheless it makes the DSL syntax much simpler and expressive. Custom Syntax ------------- -For advanced DSL scenarios, it is possible to define entire expression [_syntax_][custom syntax] - +For advanced DSL scenarios, it is possible to define entire expression [_syntax_][custom syntax] &ndash essentially custom statement types. For example, the following is a SQL-like syntax for some obscure DSL operation: diff --git a/doc/src/engine/scope.md b/doc/src/engine/scope.md index 3deecc7d..27f5d9b8 100644 --- a/doc/src/engine/scope.md +++ b/doc/src/engine/scope.md @@ -1,5 +1,5 @@ `Scope` - Initializing and Maintaining State -=========================================== +================================================= {{#include ../links.md}} diff --git a/doc/src/language/dynamic.md b/doc/src/language/dynamic.md index 9de60a1c..a89c9446 100644 --- a/doc/src/language/dynamic.md +++ b/doc/src/language/dynamic.md @@ -12,27 +12,19 @@ Use `type_of()` to Get Value Type Because [`type_of()`] a `Dynamic` value returns the type of the actual value, it is usually used to perform type-specific actions based on the actual value's type. -```rust +```c let mystery = get_some_dynamic_value(); -if type_of(mystery) == "i64" { - print("Hey, I got an integer here!"); -} else if type_of(mystery) == "f64" { - print("Hey, I got a float here!"); -} else if type_of(mystery) == "string" { - print("Hey, I got a string here!"); -} else if type_of(mystery) == "bool" { - print("Hey, I got a boolean here!"); -} else if type_of(mystery) == "array" { - 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 { - print("I don't know what this is: " + type_of(mystery)); +switch mystery { + "i64" => print("Hey, I got an integer here!"), + "f64" => print("Hey, I got a float here!"), + "string" => print("Hey, I got a string here!"), + "bool" => print("Hey, I got a boolean here!"), + "array" => print("Hey, I got an array here!"), + "map" => print("Hey, I got an object map here!"), + "Fn" => print("Hey, I got a function pointer here!"), + "TestStruct" => print("Hey, I got the TestStruct custom type here!"), + _ => print("I don't know what this is: " + type_of(mystery)) } ``` diff --git a/doc/src/language/fn-namespaces.md b/doc/src/language/fn-namespaces.md index 0d798ae4..ba551367 100644 --- a/doc/src/language/fn-namespaces.md +++ b/doc/src/language/fn-namespaces.md @@ -15,10 +15,10 @@ allow combining all functions in one [`AST`] into another, forming a new, unifie In general, there are two types of _namespaces_ where functions are looked up: -| Namespace | Source | Lookup method | Sub-modules? | Variables? | -| --------- | ------------------------------------------------------------------------------------- | ------------------------------ | :----------: | :--------: | -| Global | 1) `Engine::register_XXX` API
2) [`AST`] being evaluated
3) [packages] loaded | simple function name | ignored | ignored | -| Module | [`Module`] | module-qualified function name | yes | yes | +| Namespace | Source | Lookup method | Sub-modules? | Variables? | +| --------- | ------------------------------------------------------------------------------------- | --------------------------------- | :----------: | :--------: | +| Global | 1) `Engine::register_XXX` API
2) [`AST`] being evaluated
3) [packages] loaded | simple function name | ignored | ignored | +| Module | [`Module`] | namespace-qualified function name | yes | yes | Global Namespace diff --git a/doc/src/language/switch.md b/doc/src/language/switch.md new file mode 100644 index 00000000..45b9385d --- /dev/null +++ b/doc/src/language/switch.md @@ -0,0 +1,97 @@ +`switch` Expression +=================== + +{{#include ../links.md}} + +The `switch` _expression_ allows matching on literal values, and it mostly follows Rust's +`match` syntax: + +```c +switch calc_secret_value(x) { + 1 => print("It's one!"), + 2 => { + print("It's two!"); + print("Again!"); + } + 3 => print("Go!"), + // _ is the default when no cases match + _ => print("Oops! Something's wrong: " + x) +} +``` + + +Expression, Not Statement +------------------------ + +`switch` is not a statement, but an expression. This means that a `switch` expression can +appear anywhere a regular expression can, e.g. as function call arguments. + +```c +let x = switch foo { 1 => true, _ => false }; + +func(switch foo { + "hello" => 42, + "world" => 123, + _ => 0 +}); + +// The above is somewhat equivalent to: + +let x = if foo == 1 { true } else { false }; + +if foo == "hello" { + func(42); +} else if foo == "world" { + func(123); +} else { + func(0); +} +``` + + +Array and Object Map Literals Also Work +-------------------------------------- + +The `switch` expression can match against any _literal_, including [array] and [object map] literals. + +```c +// Match on arrays +switch [foo, bar, baz] { + ["hello", 42, true] => { ... } + ["hello", 123, false] => { ... } + ["world", 1, true] => { ... } + _ => { ... } +} + +// Match on object maps +switch map { + #{ a: 1, b: 2, c: true } => { ... } + #{ a: 42, d: "hello" } => { ... } + _ => { ... } +} +``` + + +Difference From If-Else Chain +----------------------------- + +Although a `switch` expression looks _almost_ the same as an `if`-`else` chain, +there are subtle differences between the two. + +A `switch` expression matches through _hashing_ via a look-up table. +Therefore, matching is very fast. Walking down an `if`-`else` chain +will be _much_ slower. + +On the other hand, operators can be [overloaded][operator overloading] in Rhai, +meaning that it is possible to override the `==` operator for integers such +that `if x == y` returns a different result from the built-in default. + +`switch` expressions do _not_ use the `==` operator for comparison; +instead, they _hash_ the data values and jump directly to the correct +statements via a pre-compiled look-up table. This makes matching extremely +efficient, but it also means that [overloading][operator overloading] +the `==` operator will have no effect. + +Therefore, in environments where it is desirable to [overload][operator overloading] +the `==` operator - though it is difficult to think of valid scenarios where you'd want +`1 == 1` to return something other than `true` - avoid using the `switch` expression. diff --git a/doc/src/rust/modules/create.md b/doc/src/rust/modules/create.md index 51462b22..069e5896 100644 --- a/doc/src/rust/modules/create.md +++ b/doc/src/rust/modules/create.md @@ -69,9 +69,9 @@ resolver.insert("question", module); let mut engine = Engine::new(); engine.set_module_resolver(Some(resolver)); -// Use module-qualified variables +// Use namespace-qualified variables engine.eval::(r#"import "question" as q; q::answer + 1"#)? == 42; -// Call module-qualified functions +// Call namespace-qualified functions engine.eval::(r#"import "question" as q; q::inc(q::answer)"#)? == 42; ``` diff --git a/src/ast.rs b/src/ast.rs index e5409b24..b0a4ac00 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -2,10 +2,10 @@ use crate::dynamic::{Dynamic, Union}; use crate::fn_native::{FnPtr, Shared}; -use crate::module::{Module, ModuleRef}; +use crate::module::{Module, NamespaceRef}; use crate::syntax::FnCustomSyntaxEval; use crate::token::{Position, Token, NO_POS}; -use crate::utils::ImmutableString; +use crate::utils::{ImmutableString, StraightHasherBuilder}; use crate::StaticVec; use crate::INT; @@ -16,7 +16,7 @@ use crate::FLOAT; use crate::engine::Array; #[cfg(not(feature = "no_object"))] -use crate::engine::{make_getter, make_setter, Map}; +use crate::engine::Map; #[cfg(not(feature = "no_module"))] use crate::engine::Imports; @@ -25,8 +25,9 @@ use crate::stdlib::{ any::TypeId, borrow::Cow, boxed::Box, + collections::HashMap, fmt, - hash::{Hash, Hasher}, + hash::Hash, num::NonZeroUsize, ops::{Add, AddAssign}, string::String, @@ -34,9 +35,6 @@ use crate::stdlib::{ vec::Vec, }; -#[cfg(not(feature = "no_float"))] -use crate::stdlib::ops::Neg; - #[cfg(not(feature = "no_closure"))] use crate::stdlib::collections::HashSet; @@ -98,10 +96,10 @@ pub struct ScriptFnDef { /// Function access mode. pub access: FnAccess, /// Names of function parameters. - pub params: StaticVec, + pub params: StaticVec, /// Access to external variables. #[cfg(not(feature = "no_closure"))] - pub externals: HashSet, + pub externals: HashSet, } impl fmt::Display for ScriptFnDef { @@ -602,7 +600,7 @@ pub enum ReturnType { /// ## WARNING /// /// This type is volatile and may change. -#[derive(Debug, Clone, Hash)] +#[derive(Debug, Clone)] pub enum Stmt { /// No-op. Noop(Position), @@ -637,10 +635,10 @@ pub enum Stmt { Import(Expr, Option>, Position), /// export var as var, ... #[cfg(not(feature = "no_module"))] - Export(Vec<(Ident, Option)>, Position), + Export(Vec<(IdentX, Option)>, Position), /// Convert a variable to shared. #[cfg(not(feature = "no_closure"))] - Share(Box), + Share(IdentX), } impl Default for Stmt { @@ -778,7 +776,7 @@ impl Stmt { } } -/// _[INTERNALS]_ A type wrapping a custom syntax definition. +/// _[INTERNALS]_ A custom syntax definition. /// Exported under the `internals` feature only. /// /// ## WARNING @@ -799,13 +797,6 @@ impl fmt::Debug for CustomExpr { } } -impl Hash for CustomExpr { - #[inline(always)] - fn hash(&self, state: &mut H) { - self.keywords.hash(state); - } -} - impl CustomExpr { /// Get the keywords for this `CustomExpr`. #[inline(always)] @@ -819,50 +810,13 @@ impl CustomExpr { } } -/// _[INTERNALS]_ A type wrapping a floating-point number. -/// Exported under the `internals` feature only. -/// -/// This type is mainly used to provide a standard `Hash` implementation -/// to floating-point numbers, allowing `Expr` to derive `Hash` automatically. -/// -/// ## WARNING -/// -/// This type is volatile and may change. -#[cfg(not(feature = "no_float"))] -#[derive(Debug, PartialEq, PartialOrd, Clone)] -pub struct FloatWrapper(pub FLOAT); - -#[cfg(not(feature = "no_float"))] -impl Hash for FloatWrapper { - #[inline(always)] - fn hash(&self, state: &mut H) { - state.write(&self.0.to_le_bytes()); - } -} - -#[cfg(not(feature = "no_float"))] -impl Neg for FloatWrapper { - type Output = Self; - - fn neg(self) -> Self::Output { - Self(-self.0) - } -} - -#[cfg(not(feature = "no_float"))] -impl From for FloatWrapper { - fn from(value: INT) -> Self { - Self(value as FLOAT) - } -} - -/// A binary expression structure. +/// _[INTERNALS]_ A binary expression. /// Exported under the `internals` feature only. /// /// ## WARNING /// /// This type is volatile and may change. -#[derive(Debug, Clone, Hash)] +#[derive(Debug, Clone)] pub struct BinaryExpr { /// LHS expression. pub lhs: Expr, @@ -876,8 +830,8 @@ pub struct BinaryExpr { /// ## WARNING /// /// This type is volatile and may change. -#[derive(Debug, Clone, Hash, Default)] -pub struct FnCallInfo { +#[derive(Debug, Clone, Default)] +pub struct FnCallExpr { /// Pre-calculated hash for a script-defined function of the same name and number of parameters. pub hash: u64, /// Call native functions only? Set to `true` to skip searching for script-defined function overrides @@ -889,7 +843,7 @@ pub struct FnCallInfo { /// Type is `bool` in order for `FnCallInfo` to be `Hash` pub def_value: Option, /// Namespace of the function, if any. Boxed because it occurs rarely. - pub namespace: Option>, + pub namespace: Option>, /// Function name. /// Use `Cow<'static, str>` because a lot of operators (e.g. `==`, `>=`) are implemented as function calls /// and the function names are predictable, so no need to allocate a new `String`. @@ -904,49 +858,58 @@ pub struct FnCallInfo { /// ## WARNING /// /// This type is volatile and may change. -#[derive(Debug, Clone, Hash)] +#[derive(Debug, Clone)] pub enum Expr { /// Integer constant. IntegerConstant(INT, Position), /// Floating-point constant. #[cfg(not(feature = "no_float"))] - FloatConstant(FloatWrapper, Position), + FloatConstant(FLOAT, Position), /// Character constant. CharConstant(char, Position), /// String constant. - StringConstant(Box), + StringConstant(ImmutableString, Position), /// FnPtr constant. - FnPointer(Box), - /// Variable access - (optional index, optional modules, hash, variable name) - Variable(Box<(Option, Option>, u64, Ident)>), - /// Property access - (getter, setter), prop - Property(Box<((String, String), IdentX)>), - /// { stmt } - Stmt(Box>, Position), - /// Wrapped expression - should not be optimized away. - Expr(Box), - /// func(expr, ... ) - FnCall(Box, Position), - /// lhs.rhs - Dot(Box, Position), - /// expr[expr] - Index(Box, Position), + FnPointer(ImmutableString, Position), /// [ expr, ... ] Array(Box>, Position), /// #{ name:expr, ... } Map(Box>, Position), - /// lhs in rhs - In(Box, Position), - /// lhs && rhs - And(Box, Position), - /// lhs || rhs - Or(Box, Position), /// true True(Position), /// false False(Position), /// () Unit(Position), + /// Variable access - (optional index, optional modules, hash, variable name) + Variable(Box<(Option, Option>, u64, IdentX)>), + /// Property access - (getter, setter), prop + Property(Box<((ImmutableString, ImmutableString), IdentX)>), + /// { stmt } + Stmt(Box>, Position), + /// Wrapped expression - should not be optimized away. + Expr(Box), + /// func(expr, ... ) + FnCall(Box, Position), + /// lhs.rhs + Dot(Box, Position), + /// expr[expr] + Index(Box, Position), + /// switch expr { literal or _ => stmt, ... } + Switch( + Box<( + Expr, + HashMap, + Option, + )>, + Position, + ), + /// lhs in rhs + In(Box, Position), + /// lhs && rhs + And(Box, Position), + /// lhs || rhs + Or(Box, Position), /// Custom syntax Custom(Box, Position), } @@ -970,8 +933,8 @@ impl Expr { #[cfg(not(feature = "no_float"))] Self::FloatConstant(_, _) => TypeId::of::(), Self::CharConstant(_, _) => TypeId::of::(), - Self::StringConstant(_) => TypeId::of::(), - Self::FnPointer(_) => TypeId::of::(), + Self::StringConstant(_, _) => TypeId::of::(), + Self::FnPointer(_, _) => TypeId::of::(), Self::True(_) | Self::False(_) | Self::In(_, _) | Self::And(_, _) | Self::Or(_, _) => { TypeId::of::() } @@ -996,11 +959,11 @@ impl Expr { Self::IntegerConstant(x, _) => (*x).into(), #[cfg(not(feature = "no_float"))] - Self::FloatConstant(x, _) => x.0.into(), + Self::FloatConstant(x, _) => (*x).into(), Self::CharConstant(x, _) => (*x).into(), - Self::StringConstant(x) => x.name.clone().into(), - Self::FnPointer(x) => Dynamic(Union::FnPtr(Box::new(FnPtr::new_unchecked( - x.name.clone(), + Self::StringConstant(x, _) => x.clone().into(), + Self::FnPointer(x, _) => Dynamic(Union::FnPtr(Box::new(FnPtr::new_unchecked( + x.clone(), Default::default(), )))), Self::True(_) => true.into(), @@ -1043,14 +1006,15 @@ impl Expr { Self::IntegerConstant(_, pos) => *pos, Self::CharConstant(_, pos) => *pos, - Self::StringConstant(x) => x.pos, - Self::FnPointer(x) => x.pos, + Self::StringConstant(_, pos) => *pos, + Self::FnPointer(_, pos) => *pos, Self::Array(_, pos) => *pos, Self::Map(_, pos) => *pos, Self::Property(x) => (x.1).pos, Self::Stmt(_, pos) => *pos, Self::Variable(x) => (x.3).pos, Self::FnCall(_, pos) => *pos, + Self::Switch(_, pos) => *pos, Self::And(x, _) | Self::Or(x, _) | Self::In(x, _) => x.lhs.position(), @@ -1074,14 +1038,15 @@ impl Expr { Self::IntegerConstant(_, pos) => *pos = new_pos, Self::CharConstant(_, pos) => *pos = new_pos, - Self::StringConstant(x) => x.pos = new_pos, - Self::FnPointer(x) => x.pos = new_pos, + Self::StringConstant(_, pos) => *pos = new_pos, + Self::FnPointer(_, pos) => *pos = new_pos, Self::Array(_, pos) => *pos = new_pos, Self::Map(_, pos) => *pos = new_pos, Self::Variable(x) => (x.3).pos = new_pos, Self::Property(x) => (x.1).pos = new_pos, Self::Stmt(_, pos) => *pos = new_pos, Self::FnCall(_, pos) => *pos = new_pos, + Self::Switch(_, pos) => *pos = new_pos, Self::And(_, pos) | Self::Or(_, pos) | Self::In(_, pos) => *pos = new_pos, Self::True(pos) | Self::False(pos) | Self::Unit(pos) => *pos = new_pos, Self::Dot(_, pos) | Self::Index(_, pos) => *pos = new_pos, @@ -1123,39 +1088,6 @@ impl Expr { } } - /// Is the expression a simple constant literal? - pub fn is_literal(&self) -> bool { - match self { - Self::Expr(x) => x.is_literal(), - - #[cfg(not(feature = "no_float"))] - Self::FloatConstant(_, _) => true, - - Self::IntegerConstant(_, _) - | Self::CharConstant(_, _) - | Self::StringConstant(_) - | Self::FnPointer(_) - | Self::True(_) - | Self::False(_) - | Self::Unit(_) => true, - - // An array literal is literal if all items are literals - Self::Array(x, _) => x.iter().all(Self::is_literal), - - // An map literal is literal if all items are literals - Self::Map(x, _) => x.iter().map(|(_, expr)| expr).all(Self::is_literal), - - // Check in expression - Self::In(x, _) => match (&x.lhs, &x.rhs) { - (Self::StringConstant(_), Self::StringConstant(_)) - | (Self::CharConstant(_, _), Self::StringConstant(_)) => true, - _ => false, - }, - - _ => false, - } - } - /// Is the expression a constant? pub fn is_constant(&self) -> bool { match self { @@ -1166,8 +1098,8 @@ impl Expr { Self::IntegerConstant(_, _) | Self::CharConstant(_, _) - | Self::StringConstant(_) - | Self::FnPointer(_) + | Self::StringConstant(_, _) + | Self::FnPointer(_, _) | Self::True(_) | Self::False(_) | Self::Unit(_) => true, @@ -1180,8 +1112,8 @@ impl Expr { // Check in expression Self::In(x, _) => match (&x.lhs, &x.rhs) { - (Self::StringConstant(_), Self::StringConstant(_)) - | (Self::CharConstant(_, _), Self::StringConstant(_)) => true, + (Self::StringConstant(_, _), Self::StringConstant(_, _)) + | (Self::CharConstant(_, _), Self::StringConstant(_, _)) => true, _ => false, }, @@ -1199,7 +1131,7 @@ impl Expr { Self::IntegerConstant(_, _) | Self::CharConstant(_, _) - | Self::FnPointer(_) + | Self::FnPointer(_, _) | Self::In(_, _) | Self::And(_, _) | Self::Or(_, _) @@ -1207,9 +1139,10 @@ impl Expr { | Self::False(_) | Self::Unit(_) => false, - Self::StringConstant(_) + Self::StringConstant(_, _) | Self::Stmt(_, _) | Self::FnCall(_, _) + | Self::Switch(_, _) | Self::Dot(_, _) | Self::Index(_, _) | Self::Array(_, _) @@ -1238,21 +1171,6 @@ impl Expr { Self::Custom(_, _) => false, } } - - /// Convert a `Variable` into a `Property`. All other variants are untouched. - #[cfg(not(feature = "no_object"))] - #[inline] - pub(crate) fn into_property(self) -> Self { - match self { - Self::Variable(x) if x.1.is_none() => { - let ident = x.3; - let getter = make_getter(&ident.name); - let setter = make_setter(&ident.name); - Self::Property(Box::new(((getter, setter), ident.into()))) - } - _ => self, - } - } } #[cfg(test)] diff --git a/src/dynamic.rs b/src/dynamic.rs index 6b3cc8b1..b0bee022 100644 --- a/src/dynamic.rs +++ b/src/dynamic.rs @@ -3,7 +3,7 @@ use crate::fn_native::{FnPtr, SendSync}; use crate::r#unsafe::{unsafe_cast_box, unsafe_try_cast}; use crate::utils::ImmutableString; -use crate::INT; +use crate::{StaticVec, INT}; #[cfg(not(feature = "no_closure"))] use crate::fn_native::{shared_try_take, Locked, Shared}; @@ -21,6 +21,7 @@ use crate::stdlib::{ any::{type_name, Any, TypeId}, boxed::Box, fmt, + hash::{Hash, Hasher}, ops::{Deref, DerefMut}, string::{String, ToString}, }; @@ -361,6 +362,44 @@ impl Dynamic { } } +impl Hash for Dynamic { + fn hash(&self, state: &mut H) { + match &self.0 { + Union::Unit(_) => ().hash(state), + Union::Bool(value) => value.hash(state), + Union::Str(s) => s.hash(state), + Union::Char(ch) => ch.hash(state), + Union::Int(i) => i.hash(state), + #[cfg(not(feature = "no_float"))] + Union::Float(f) => { + TypeId::of::().hash(state); + state.write(&f.to_le_bytes()); + } + #[cfg(not(feature = "no_index"))] + Union::Array(a) => a.hash(state), + #[cfg(not(feature = "no_object"))] + Union::Map(m) => { + let mut buf: StaticVec<_> = m.keys().collect(); + buf.sort(); + + buf.into_iter().for_each(|key| { + key.hash(state); + m[key].hash(state); + }) + } + + #[cfg(not(feature = "no_closure"))] + #[cfg(not(feature = "sync"))] + Union::Shared(cell) => (*cell.borrow()).hash(state), + #[cfg(not(feature = "no_closure"))] + #[cfg(feature = "sync")] + Union::Shared(cell) => (*cell.read().unwrap()).hash(state), + + _ => unimplemented!(), + } + } +} + /// Map the name of a standard type into a friendly form. #[inline] pub(crate) fn map_std_type_name(name: &str) -> &str { diff --git a/src/engine.rs b/src/engine.rs index 57ced10c..16558bb5 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,10 +1,10 @@ //! Main module defining the script evaluation `Engine`. -use crate::ast::{BinaryExpr, Expr, FnCallInfo, Ident, IdentX, ReturnType, Stmt}; +use crate::ast::{BinaryExpr, Expr, FnCallExpr, Ident, IdentX, ReturnType, Stmt}; use crate::dynamic::{map_std_type_name, Dynamic, Union, Variant}; use crate::fn_call::run_builtin_op_assignment; use crate::fn_native::{Callback, FnPtr, OnVarCallback, Shared}; -use crate::module::{Module, ModuleRef}; +use crate::module::{Module, NamespaceRef}; use crate::optimize::OptimizationLevel; use crate::packages::{Package, PackagesCollection, StandardPackage}; use crate::r#unsafe::unsafe_cast_var_name_to_lifetime; @@ -12,6 +12,7 @@ use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; use crate::syntax::CustomSyntax; use crate::token::{Position, NO_POS}; +use crate::utils::get_hasher; use crate::{calc_native_fn_hash, StaticVec}; #[cfg(not(feature = "no_index"))] @@ -38,6 +39,7 @@ use crate::stdlib::{ boxed::Box, collections::{HashMap, HashSet}, fmt, format, + hash::{Hash, Hasher}, iter::{empty, once}, num::NonZeroUsize, ops::DerefMut, @@ -593,7 +595,7 @@ pub struct Engine { /// Max limits. #[cfg(not(feature = "unchecked"))] - pub(crate) limits_set: Limits, + pub(crate) limits: Limits, } impl fmt::Debug for Engine { @@ -636,6 +638,7 @@ pub fn is_anonymous_fn(fn_name: &str) -> bool { } /// Print/debug to stdout +#[inline(always)] fn default_print(_s: &str) { #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] @@ -647,15 +650,15 @@ fn default_print(_s: &str) { pub fn search_imports( mods: &Imports, state: &mut State, - modules: &ModuleRef, + namespace: &NamespaceRef, ) -> Result, Box> { - let Ident { name: root, pos } = &modules[0]; + let IdentX { name: root, pos } = &namespace[0]; // Qualified - check if the root module is directly indexed let index = if state.always_search { 0 } else { - modules.index().map_or(0, NonZeroUsize::get) + namespace.index().map_or(0, NonZeroUsize::get) }; Ok(if index > 0 { @@ -670,7 +673,7 @@ pub fn search_imports( impl Engine { /// Create a new `Engine` - #[inline(always)] + #[inline] pub fn new() -> Self { // Create the new scripting Engine let mut engine = Self { @@ -710,7 +713,7 @@ impl Engine { }, #[cfg(not(feature = "unchecked"))] - limits_set: Limits { + limits: Limits { max_call_stack_depth: MAX_CALL_STACK_DEPTH, max_expr_depth: MAX_EXPR_DEPTH, #[cfg(not(feature = "no_function"))] @@ -733,7 +736,7 @@ impl Engine { /// Create a new `Engine` with minimal built-in functions. /// Use the `load_package` method to load additional packages of functions. - #[inline(always)] + #[inline] pub fn new_raw() -> Self { Self { id: Default::default(), @@ -762,7 +765,7 @@ impl Engine { }, #[cfg(not(feature = "unchecked"))] - limits_set: Limits { + limits: Limits { max_call_stack_depth: MAX_CALL_STACK_DEPTH, max_expr_depth: MAX_EXPR_DEPTH, #[cfg(not(feature = "no_function"))] @@ -780,7 +783,7 @@ impl Engine { } /// Search for a variable within the scope or within imports, - /// depending on whether the variable name is qualified. + /// depending on whether the variable name is namespace-qualified. pub(crate) fn search_namespace<'s, 'a>( &self, scope: &'s mut Scope, @@ -793,7 +796,7 @@ impl Engine { match expr { Expr::Variable(v) => match v.as_ref() { // Qualified variable - (_, Some(modules), hash_var, Ident { name, pos }) => { + (_, Some(modules), hash_var, IdentX { name, pos }) => { let module = search_imports(mods, state, modules)?; let target = module.get_qualified_var(*hash_var).map_err(|mut err| { match *err { @@ -825,13 +828,13 @@ impl Engine { this_ptr: &'s mut Option<&mut Dynamic>, expr: &'a Expr, ) -> Result<(Target<'s>, &'a str, ScopeEntryType, Position), Box> { - let (index, _, _, Ident { name, pos }) = match expr { + let (index, _, _, IdentX { name, pos }) = match expr { Expr::Variable(v) => v.as_ref(), _ => unreachable!(), }; // Check if the variable is `this` - if name == KEYWORD_THIS { + if name.as_str() == KEYWORD_THIS { if let Some(val) = this_ptr { return Ok(((*val).into(), KEYWORD_THIS, ScopeEntryType::Normal, *pos)); } else { @@ -870,7 +873,7 @@ impl Engine { // Find the variable in the scope scope .get_index(name) - .ok_or_else(|| EvalAltResult::ErrorVariableNotFound(name.into(), *pos))? + .ok_or_else(|| EvalAltResult::ErrorVariableNotFound(name.to_string(), *pos))? .0 }; @@ -1000,7 +1003,7 @@ impl Engine { match rhs { // xxx.fn_name(arg_expr_list) Expr::FnCall(x, pos) if x.namespace.is_none() => { - let FnCallInfo { + let FnCallExpr { name, native_only: native, hash, @@ -1073,7 +1076,7 @@ impl Engine { } // {xxx:map}.fn_name(arg_expr_list)[expr] | {xxx:map}.fn_name(arg_expr_list).expr Expr::FnCall(x, pos) if x.namespace.is_none() => { - let FnCallInfo { + let FnCallExpr { name, native_only: native, hash, @@ -1157,7 +1160,7 @@ impl Engine { } // xxx.fn_name(arg_expr_list)[expr] | xxx.fn_name(arg_expr_list).expr Expr::FnCall(f, pos) if f.namespace.is_none() => { - let FnCallInfo { + let FnCallExpr { name, native_only: native, hash, @@ -1209,14 +1212,7 @@ impl Engine { level: usize, new_val: Option<(Dynamic, Position)>, ) -> Result> { - let ( - BinaryExpr { - lhs: dot_lhs, - rhs: dot_rhs, - }, - chain_type, - op_pos, - ) = match expr { + let (BinaryExpr { lhs, rhs }, chain_type, op_pos) = match expr { Expr::Index(x, pos) => (x.as_ref(), ChainType::Index, *pos), Expr::Dot(x, pos) => (x.as_ref(), ChainType::Dot, *pos), _ => unreachable!(), @@ -1230,17 +1226,17 @@ impl Engine { state, lib, this_ptr, - dot_rhs, + rhs, chain_type, &mut idx_values, 0, level, )?; - match dot_lhs { + match lhs { // id.??? or id[???] Expr::Variable(x) => { - let Ident { + let IdentX { name: var_name, pos: var_pos, } = &x.3; @@ -1249,7 +1245,7 @@ impl Engine { .map_err(|err| err.fill_position(*var_pos))?; let (target, _, typ, pos) = - self.search_namespace(scope, mods, state, lib, this_ptr, dot_lhs)?; + self.search_namespace(scope, mods, state, lib, this_ptr, lhs)?; // Constants cannot be modified match typ { @@ -1262,7 +1258,7 @@ impl Engine { let obj_ptr = &mut target.into(); self.eval_dot_index_chain_helper( - mods, state, lib, &mut None, obj_ptr, dot_rhs, idx_values, chain_type, level, + mods, state, lib, &mut None, obj_ptr, rhs, idx_values, chain_type, level, new_val, ) .map(|(v, _)| v) @@ -1275,7 +1271,7 @@ impl Engine { 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( - mods, state, lib, this_ptr, obj_ptr, dot_rhs, idx_values, chain_type, level, + mods, state, lib, this_ptr, obj_ptr, rhs, idx_values, chain_type, level, new_val, ) .map(|(v, _)| v) @@ -1549,12 +1545,10 @@ impl Engine { Expr::IntegerConstant(x, _) => Ok((*x).into()), #[cfg(not(feature = "no_float"))] - Expr::FloatConstant(x, _) => Ok(x.0.into()), - Expr::StringConstant(x) => Ok(x.name.clone().into()), + Expr::FloatConstant(x, _) => Ok((*x).into()), + Expr::StringConstant(x, _) => Ok(x.clone().into()), Expr::CharConstant(x, _) => Ok((*x).into()), - Expr::FnPointer(x) => { - Ok(FnPtr::new_unchecked(x.name.clone(), Default::default()).into()) - } + Expr::FnPointer(x, _) => Ok(FnPtr::new_unchecked(x.clone(), Default::default()).into()), Expr::Variable(x) if (x.3).name == KEYWORD_THIS => { if let Some(val) = this_ptr { Ok(val.clone()) @@ -1605,7 +1599,7 @@ impl Engine { // Normal function call Expr::FnCall(x, pos) if x.namespace.is_none() => { - let FnCallInfo { + let FnCallExpr { name, native_only: native, capture: cap_scope, @@ -1622,9 +1616,9 @@ impl Engine { .map_err(|err| err.fill_position(*pos)) } - // Module-qualified function call + // Namespace-qualified function call Expr::FnCall(x, pos) if x.namespace.is_some() => { - let FnCallInfo { + let FnCallExpr { name, namespace, hash, @@ -1632,9 +1626,9 @@ impl Engine { def_value, .. } = x.as_ref(); - let modules = namespace.as_ref().map(|v| v.as_ref()); + let namespace = namespace.as_ref().map(|v| v.as_ref()); self.make_qualified_function_call( - scope, mods, state, lib, this_ptr, modules, name, args, *def_value, *hash, + scope, mods, state, lib, this_ptr, namespace, name, args, *def_value, *hash, level, ) .map_err(|err| err.fill_position(*pos)) @@ -1674,6 +1668,25 @@ impl Engine { Expr::False(_) => Ok(false.into()), Expr::Unit(_) => Ok(().into()), + Expr::Switch(x, _) => { + let (match_expr, table, def_stmt) = x.as_ref(); + + let match_item = + self.eval_expr(scope, mods, state, lib, this_ptr, match_expr, level)?; + + let hasher = &mut get_hasher(); + match_item.hash(hasher); + let hash = hasher.finish(); + + if let Some(stmt) = table.get(&hash) { + self.eval_stmt(scope, mods, state, lib, this_ptr, stmt, level) + } else if let Some(def_stmt) = def_stmt { + self.eval_stmt(scope, mods, state, lib, this_ptr, def_stmt, level) + } else { + Ok(().into()) + } + } + Expr::Custom(custom, _) => { let expressions = custom .keywords() @@ -2184,13 +2197,14 @@ impl Engine { // Export statement #[cfg(not(feature = "no_module"))] Stmt::Export(list, _) => { - for (Ident { name, pos: id_pos }, rename) in list.iter() { + for (IdentX { name, pos: id_pos }, rename) in list.iter() { // Mark scope variables as public if let Some(index) = scope.get_index(name).map(|(i, _)| i) { let alias = rename.as_ref().map(|x| &x.name).unwrap_or_else(|| name); - scope.add_entry_alias(index, alias.clone()); + scope.add_entry_alias(index, alias.to_string()); } else { - return EvalAltResult::ErrorVariableNotFound(name.into(), *id_pos).into(); + return EvalAltResult::ErrorVariableNotFound(name.to_string(), *id_pos) + .into(); } } Ok(Default::default()) diff --git a/src/engine_api.rs b/src/engine_api.rs index 084c2040..7db48d05 100644 --- a/src/engine_api.rs +++ b/src/engine_api.rs @@ -9,6 +9,7 @@ use crate::parse_error::ParseError; use crate::result::EvalAltResult; use crate::scope::Scope; use crate::token::{Position, NO_POS}; +use crate::utils::get_hasher; #[cfg(not(feature = "no_index"))] use crate::{ @@ -35,6 +36,7 @@ use crate::optimize::optimize_into_ast; use crate::stdlib::{ any::{type_name, TypeId}, boxed::Box, + hash::{Hash, Hasher}, string::String, }; @@ -45,6 +47,13 @@ use crate::stdlib::mem; #[cfg(not(target_arch = "wasm32"))] use crate::stdlib::{fs::File, io::prelude::*, path::PathBuf}; +/// Calculate a unique hash for a script. +fn calc_hash_for_scripts<'a>(scripts: impl IntoIterator) -> u64 { + let s = &mut get_hasher(); + scripts.into_iter().for_each(|&script| script.hash(s)); + s.finish() +} + /// Engine public API impl Engine { /// Register a function of the `Engine`. @@ -907,8 +916,9 @@ impl Engine { scripts: &[&str], optimization_level: OptimizationLevel, ) -> Result { + let hash = calc_hash_for_scripts(scripts); let stream = self.lex(scripts, None); - self.parse(&mut stream.peekable(), scope, optimization_level) + self.parse(hash, &mut stream.peekable(), scope, optimization_level) } /// Read the contents of a file into a string. @@ -1061,6 +1071,7 @@ impl Engine { .into()); }; + let hash = calc_hash_for_scripts(&scripts); let stream = self.lex( &scripts, if has_null { @@ -1073,8 +1084,12 @@ impl Engine { None }, ); - let ast = - self.parse_global_expr(&mut stream.peekable(), &scope, OptimizationLevel::None)?; + let ast = self.parse_global_expr( + hash, + &mut stream.peekable(), + &scope, + OptimizationLevel::None, + )?; // Handle null - map to () if has_null { @@ -1155,11 +1170,11 @@ impl Engine { script: &str, ) -> Result { let scripts = [script]; + let hash = calc_hash_for_scripts(&scripts); let stream = self.lex(&scripts, None); - { - let mut peekable = stream.peekable(); - self.parse_global_expr(&mut peekable, scope, self.optimization_level) - } + + let mut peekable = stream.peekable(); + self.parse_global_expr(hash, &mut peekable, scope, self.optimization_level) } /// Evaluate a script file. @@ -1316,10 +1331,12 @@ impl Engine { script: &str, ) -> Result> { let scripts = [script]; + let hash = calc_hash_for_scripts(&scripts); let stream = self.lex(&scripts, None); // No need to optimize a lone expression - let ast = self.parse_global_expr(&mut stream.peekable(), scope, OptimizationLevel::None)?; + let ast = + self.parse_global_expr(hash, &mut stream.peekable(), scope, OptimizationLevel::None)?; self.eval_ast_with_scope(scope, &ast) } @@ -1446,8 +1463,9 @@ impl Engine { script: &str, ) -> Result<(), Box> { let scripts = [script]; + let hash = calc_hash_for_scripts(&scripts); let stream = self.lex(&scripts, None); - let ast = self.parse(&mut stream.peekable(), scope, self.optimization_level)?; + let ast = self.parse(hash, &mut stream.peekable(), scope, self.optimization_level)?; self.consume_ast_with_scope(scope, &ast) } diff --git a/src/engine_settings.rs b/src/engine_settings.rs index 987187f1..b6d7cfd9 100644 --- a/src/engine_settings.rs +++ b/src/engine_settings.rs @@ -53,7 +53,7 @@ impl Engine { #[cfg(not(feature = "unchecked"))] #[inline(always)] pub fn set_max_call_levels(&mut self, levels: usize) -> &mut Self { - self.limits_set.max_call_stack_depth = levels; + self.limits.max_call_stack_depth = levels; self } @@ -61,7 +61,7 @@ impl Engine { #[cfg(not(feature = "unchecked"))] #[inline(always)] pub fn max_call_levels(&self) -> usize { - self.limits_set.max_call_stack_depth + self.limits.max_call_stack_depth } /// Set the maximum number of operations allowed for a script to run to avoid @@ -69,7 +69,7 @@ impl Engine { #[cfg(not(feature = "unchecked"))] #[inline(always)] pub fn set_max_operations(&mut self, operations: u64) -> &mut Self { - self.limits_set.max_operations = if operations == u64::MAX { + self.limits.max_operations = if operations == u64::MAX { 0 } else { operations @@ -81,7 +81,7 @@ impl Engine { #[cfg(not(feature = "unchecked"))] #[inline(always)] pub fn max_operations(&self) -> u64 { - self.limits_set.max_operations + self.limits.max_operations } /// Set the maximum number of imported modules allowed for a script. @@ -89,7 +89,7 @@ impl Engine { #[cfg(not(feature = "no_module"))] #[inline(always)] pub fn set_max_modules(&mut self, modules: usize) -> &mut Self { - self.limits_set.max_modules = modules; + self.limits.max_modules = modules; self } @@ -98,7 +98,7 @@ impl Engine { #[cfg(not(feature = "no_module"))] #[inline(always)] pub fn max_modules(&self) -> usize { - self.limits_set.max_modules + self.limits.max_modules } /// Set the depth limits for expressions (0 for unlimited). @@ -109,14 +109,14 @@ impl Engine { max_expr_depth: usize, #[cfg(not(feature = "no_function"))] max_function_expr_depth: usize, ) -> &mut Self { - self.limits_set.max_expr_depth = if max_expr_depth == usize::MAX { + self.limits.max_expr_depth = if max_expr_depth == usize::MAX { 0 } else { max_expr_depth }; #[cfg(not(feature = "no_function"))] { - self.limits_set.max_function_expr_depth = if max_function_expr_depth == usize::MAX { + self.limits.max_function_expr_depth = if max_function_expr_depth == usize::MAX { 0 } else { max_function_expr_depth @@ -129,7 +129,7 @@ impl Engine { #[cfg(not(feature = "unchecked"))] #[inline(always)] pub fn max_expr_depth(&self) -> usize { - self.limits_set.max_expr_depth + self.limits.max_expr_depth } /// The depth limit for expressions in functions (0 for unlimited). @@ -137,14 +137,14 @@ impl Engine { #[cfg(not(feature = "no_function"))] #[inline(always)] pub fn max_function_expr_depth(&self) -> usize { - self.limits_set.max_function_expr_depth + self.limits.max_function_expr_depth } /// Set the maximum length of strings (0 for unlimited). #[cfg(not(feature = "unchecked"))] #[inline(always)] pub fn set_max_string_size(&mut self, max_size: usize) -> &mut Self { - self.limits_set.max_string_size = if max_size == usize::MAX { 0 } else { max_size }; + self.limits.max_string_size = if max_size == usize::MAX { 0 } else { max_size }; self } @@ -152,7 +152,7 @@ impl Engine { #[cfg(not(feature = "unchecked"))] #[inline(always)] pub fn max_string_size(&self) -> usize { - self.limits_set.max_string_size + self.limits.max_string_size } /// Set the maximum length of arrays (0 for unlimited). @@ -160,7 +160,7 @@ impl Engine { #[cfg(not(feature = "no_index"))] #[inline(always)] pub fn set_max_array_size(&mut self, max_size: usize) -> &mut Self { - self.limits_set.max_array_size = if max_size == usize::MAX { 0 } else { max_size }; + self.limits.max_array_size = if max_size == usize::MAX { 0 } else { max_size }; self } @@ -169,7 +169,7 @@ impl Engine { #[cfg(not(feature = "no_index"))] #[inline(always)] pub fn max_array_size(&self) -> usize { - self.limits_set.max_array_size + self.limits.max_array_size } /// Set the maximum length of object maps (0 for unlimited). @@ -177,7 +177,7 @@ impl Engine { #[cfg(not(feature = "no_object"))] #[inline(always)] pub fn set_max_map_size(&mut self, max_size: usize) -> &mut Self { - self.limits_set.max_map_size = if max_size == usize::MAX { 0 } else { max_size }; + self.limits.max_map_size = if max_size == usize::MAX { 0 } else { max_size }; self } @@ -186,7 +186,7 @@ impl Engine { #[cfg(not(feature = "no_object"))] #[inline(always)] pub fn max_map_size(&self) -> usize { - self.limits_set.max_map_size + self.limits.max_map_size } /// Set the module resolution service used by the `Engine`. diff --git a/src/fn_call.rs b/src/fn_call.rs index 3c258f93..cc740594 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -8,7 +8,7 @@ use crate::engine::{ KEYWORD_PRINT, KEYWORD_TYPE_OF, }; use crate::fn_native::{FnCallArgs, FnPtr}; -use crate::module::{Module, ModuleRef}; +use crate::module::{Module, NamespaceRef}; use crate::optimize::OptimizationLevel; use crate::parse_error::ParseErrorType; use crate::result::EvalAltResult; @@ -432,13 +432,13 @@ impl Engine { pub(crate) fn has_override_by_name_and_arguments( &self, lib: &[&Module], - name: &str, + fn_name: &str, arg_types: impl AsRef<[TypeId]>, pub_only: bool, ) -> bool { let arg_types = arg_types.as_ref(); - let hash_fn = calc_native_fn_hash(empty(), name, arg_types.iter().cloned()); - let hash_script = calc_script_fn_hash(empty(), name, arg_types.len()); + let hash_fn = calc_native_fn_hash(empty(), fn_name, arg_types.iter().cloned()); + let hash_script = calc_script_fn_hash(empty(), fn_name, arg_types.len()); self.has_override(lib, hash_fn, hash_script, pub_only) } @@ -694,7 +694,7 @@ impl Engine { mods: &mut Imports, state: &mut State, lib: &[&Module], - name: &str, + fn_name: &str, hash_script: u64, target: &mut Target, mut call_args: StaticVec, @@ -707,9 +707,9 @@ impl Engine { // Get a reference to the mutation target Dynamic let obj = target.as_mut(); - let mut _fn_name = name; + let mut fn_name = fn_name; - let (result, updated) = if _fn_name == KEYWORD_FN_PTR_CALL && obj.is::() { + let (result, updated) = if fn_name == KEYWORD_FN_PTR_CALL && obj.is::() { // FnPtr call let fn_ptr = obj.read_lock::().unwrap(); // Redirect function name @@ -733,7 +733,7 @@ impl Engine { self.exec_fn_call( mods, state, lib, fn_name, hash, args, false, false, pub_only, None, def_val, level, ) - } else if _fn_name == KEYWORD_FN_PTR_CALL + } else if fn_name == KEYWORD_FN_PTR_CALL && call_args.len() > 0 && call_args[0].is::() { @@ -760,7 +760,7 @@ impl Engine { self.exec_fn_call( mods, state, lib, fn_name, hash, args, is_ref, true, pub_only, None, def_val, level, ) - } else if _fn_name == KEYWORD_FN_PTR_CURRY && obj.is::() { + } else if fn_name == KEYWORD_FN_PTR_CURRY && obj.is::() { // Curry call let fn_ptr = obj.read_lock::().unwrap(); Ok(( @@ -779,7 +779,7 @@ impl Engine { } else if { #[cfg(not(feature = "no_closure"))] { - _fn_name == KEYWORD_IS_SHARED && call_args.is_empty() + fn_name == KEYWORD_IS_SHARED && call_args.is_empty() } #[cfg(feature = "no_closure")] false @@ -793,11 +793,11 @@ impl Engine { // Check if it is a map method call in OOP style #[cfg(not(feature = "no_object"))] if let Some(map) = obj.read_lock::() { - if let Some(val) = map.get(_fn_name) { + if let Some(val) = map.get(fn_name) { if let Some(fn_ptr) = val.read_lock::() { // Remap the function name _redirected = fn_ptr.get_fn_name().clone(); - _fn_name = &_redirected; + fn_name = &_redirected; // Add curried arguments fn_ptr .curry() @@ -809,7 +809,7 @@ impl Engine { hash = if native { 0 } else { - calc_script_fn_hash(empty(), _fn_name, call_args.len()) + calc_script_fn_hash(empty(), fn_name, call_args.len()) }; } } @@ -826,8 +826,7 @@ impl Engine { let args = arg_values.as_mut(); self.exec_fn_call( - mods, state, lib, _fn_name, hash, args, is_ref, true, pub_only, None, def_val, - level, + mods, state, lib, fn_name, hash, args, is_ref, true, pub_only, None, def_val, level, ) }?; @@ -848,7 +847,7 @@ impl Engine { state: &mut State, lib: &[&Module], this_ptr: &mut Option<&mut Dynamic>, - name: &str, + fn_name: &str, args_expr: impl AsRef<[Expr]>, def_val: Option, mut hash_script: u64, @@ -860,8 +859,9 @@ impl Engine { let args_expr = args_expr.as_ref(); // Handle Fn() - if name == KEYWORD_FN_PTR && args_expr.len() == 1 { - let hash_fn = calc_native_fn_hash(empty(), name, once(TypeId::of::())); + if fn_name == KEYWORD_FN_PTR && args_expr.len() == 1 { + let hash_fn = + calc_native_fn_hash(empty(), fn_name, once(TypeId::of::())); if !self.has_override(lib, hash_fn, hash_script, pub_only) { // Fn - only in function call style @@ -878,7 +878,7 @@ impl Engine { } // Handle curry() - if name == KEYWORD_FN_PTR_CURRY && args_expr.len() > 1 { + if fn_name == KEYWORD_FN_PTR_CURRY && args_expr.len() > 1 { let fn_ptr = self.eval_expr(scope, mods, state, lib, this_ptr, &args_expr[0], level)?; if !fn_ptr.is::() { @@ -905,7 +905,7 @@ impl Engine { // Handle is_shared() #[cfg(not(feature = "no_closure"))] - if name == KEYWORD_IS_SHARED && args_expr.len() == 1 { + if fn_name == KEYWORD_IS_SHARED && args_expr.len() == 1 { let value = self.eval_expr(scope, mods, state, lib, this_ptr, &args_expr[0], level)?; return Ok(value.is_shared().into()); @@ -915,7 +915,7 @@ impl Engine { let redirected; let mut args_expr = args_expr.as_ref(); let mut curry = StaticVec::new(); - let mut name = name; + let mut name = fn_name; if name == KEYWORD_FN_PTR_CALL && args_expr.len() >= 1 @@ -1078,7 +1078,7 @@ impl Engine { .map(|(v, _)| v) } - /// Call a module-qualified function in normal function-call style. + /// Call a namespace-qualified function in normal function-call style. /// Position in `EvalAltResult` is `None` and must be set afterwards. pub(crate) fn make_qualified_function_call( &self, @@ -1087,8 +1087,8 @@ impl Engine { state: &mut State, lib: &[&Module], this_ptr: &mut Option<&mut Dynamic>, - modules: Option<&ModuleRef>, - name: &str, + namespace: Option<&NamespaceRef>, + fn_name: &str, args_expr: impl AsRef<[Expr]>, def_val: Option, hash_script: u64, @@ -1096,7 +1096,7 @@ impl Engine { ) -> Result> { let args_expr = args_expr.as_ref(); - let modules = modules.as_ref().unwrap(); + let namespace = namespace.as_ref().unwrap(); let mut arg_values: StaticVec<_>; let mut first_arg_value = None; let mut args: StaticVec<_>; @@ -1105,7 +1105,7 @@ impl Engine { // No arguments args = Default::default(); } else { - // See if the first argument is a variable (not module-qualified). + // See if the first argument is a variable (not namespace-qualified). // If so, convert to method-call style in order to leverage potential // &mut first argument and avoid cloning the value if args_expr[0].get_variable_access(true).is_some() { @@ -1151,7 +1151,7 @@ impl Engine { } } - let module = search_imports(mods, state, modules)?; + let module = search_imports(mods, state, namespace)?; // First search in script-defined functions (can override built-in) let func = match module.get_qualified_fn(hash_script) { @@ -1159,7 +1159,7 @@ impl Engine { None => { self.inc_operations(state)?; - // Qualified Rust functions are indexed in two steps: + // Namespace-qualified Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, // i.e. qualifiers + function name + number of arguments. // 2) Calculate a second hash with no qualifiers, empty function name, @@ -1210,8 +1210,8 @@ impl Engine { None => EvalAltResult::ErrorFunctionNotFound( format!( "{}{} ({})", - modules, - name, + namespace, + fn_name, args.iter() .map(|a| if a.is::() { "&str | ImmutableString | String" diff --git a/src/lib.rs b/src/lib.rs index e8019d24..aeca9235 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,19 +83,20 @@ mod token; mod r#unsafe; mod utils; -/// The system integer type. +/// The system integer type. It is defined as `i64`. /// /// If the `only_i32` feature is enabled, this will be `i32` instead. #[cfg(not(feature = "only_i32"))] pub type INT = i64; /// The system integer type. +/// It is defined as `i32` since the `only_i32` feature is used. /// /// If the `only_i32` feature is not enabled, this will be `i64` instead. #[cfg(feature = "only_i32")] pub type INT = i32; -/// The system floating-point type. +/// The system floating-point type. It is defined as `f64`. /// /// Not available under the `no_float` feature. #[cfg(not(feature = "no_float"))] @@ -103,6 +104,7 @@ pub type INT = i32; pub type FLOAT = f64; /// The system floating-point type. +/// It is defined as `f32` since the `f32_float` feature is used. /// /// Not available under the `no_float` feature. #[cfg(not(feature = "no_float"))] @@ -168,8 +170,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 ast::{ - BinaryExpr, CustomExpr, Expr, FloatWrapper, FnCallInfo, Ident, IdentX, ReturnType, ScriptFnDef, - Stmt, + BinaryExpr, CustomExpr, Expr, FnCallExpr, Ident, IdentX, ReturnType, ScriptFnDef, Stmt, }; #[cfg(feature = "internals")] @@ -178,7 +179,7 @@ pub use engine::{Imports, Limits, State as EvalState}; #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] -pub use module::ModuleRef; +pub use module::NamespaceRef; /// _[INTERNALS]_ Alias to [`smallvec::SmallVec<[T; 4]>`](https://crates.io/crates/smallvec), /// which is a specialized `Vec` backed by a small, fixed-size array when there are <= 4 items stored. diff --git a/src/module/mod.rs b/src/module/mod.rs index f9d28d87..9df9aec5 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -1,6 +1,6 @@ //! Module defining external-loaded modules for Rhai. -use crate::ast::{FnAccess, Ident}; +use crate::ast::{FnAccess, IdentX}; use crate::dynamic::{Dynamic, Variant}; use crate::fn_native::{ shared_make_mut, shared_take_or_clone, CallableFunction, FnCallArgs, IteratorFn, @@ -55,8 +55,8 @@ pub struct FuncInfo { pub types: Option>, } -/// An imported module, which may contain variables, sub-modules, -/// external Rust functions, and script-defined functions. +/// A module which may contain variables, sub-modules, external Rust functions, +/// and/or script-defined functions. /// /// Not available under the `no_module` feature. #[derive(Default, Clone)] @@ -253,7 +253,7 @@ impl Module { self } - /// Get a reference to a modules-qualified variable. + /// Get a reference to a namespace-qualified variable. /// Name and Position in `EvalAltResult` are None and must be set afterwards. /// /// The `u64` hash is calculated by the function `crate::calc_native_fn_hash`. @@ -1105,7 +1105,7 @@ impl Module { } } - /// Get a modules-qualified function. + /// Get a namespace-qualified function. /// Name and Position in `EvalAltResult` are None and must be set afterwards. /// /// The `u64` hash is calculated by the function `crate::calc_native_fn_hash` and must match @@ -1435,7 +1435,7 @@ impl Module { if let Some(param_types) = types { assert_eq!(*params, param_types.len()); - // Qualified Rust functions are indexed in two steps: + // Namespace-qualified Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, // i.e. qualifiers + function name + number of arguments. let hash_qualified_script = @@ -1516,21 +1516,21 @@ impl Module { } } -/// _[INTERNALS]_ A chain of module names to qualify a variable or function call. +/// _[INTERNALS]_ A chain of module names to namespace-qualify a variable or function call. /// Exported under the `internals` feature only. /// /// A `u64` hash key is cached for quick search purposes. /// -/// A `StaticVec` is used because most module-level access contains only one level, +/// A `StaticVec` is used because most namespace-qualified access contains only one level, /// and it is wasteful to always allocate a `Vec` with one element. /// /// ## WARNING /// /// This type is volatile and may change. #[derive(Clone, Eq, PartialEq, Default, Hash)] -pub struct ModuleRef(StaticVec, Option); +pub struct NamespaceRef(StaticVec, Option); -impl fmt::Debug for ModuleRef { +impl fmt::Debug for NamespaceRef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, f)?; @@ -1542,31 +1542,31 @@ impl fmt::Debug for ModuleRef { } } -impl Deref for ModuleRef { - type Target = StaticVec; +impl Deref for NamespaceRef { + type Target = StaticVec; fn deref(&self) -> &Self::Target { &self.0 } } -impl DerefMut for ModuleRef { +impl DerefMut for NamespaceRef { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -impl fmt::Display for ModuleRef { +impl fmt::Display for NamespaceRef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for Ident { name, .. } in self.0.iter() { + for IdentX { name, .. } in self.0.iter() { write!(f, "{}{}", name, Token::DoubleColon.syntax())?; } Ok(()) } } -impl From> for ModuleRef { - fn from(modules: StaticVec) -> Self { +impl From> for NamespaceRef { + fn from(modules: StaticVec) -> Self { Self(modules, None) } } @@ -1596,7 +1596,7 @@ impl> AddAssign for Module { } } -impl ModuleRef { +impl NamespaceRef { pub(crate) fn index(&self) -> Option { self.1 } diff --git a/src/optimize.rs b/src/optimize.rs index e2685662..11e4fae3 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -1,6 +1,6 @@ //! Module implementing the AST optimizer. -use crate::ast::{BinaryExpr, CustomExpr, Expr, FnCallInfo, ScriptFnDef, Stmt, AST}; +use crate::ast::{Expr, ScriptFnDef, Stmt, AST}; use crate::dynamic::Dynamic; use crate::engine::{ Engine, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_IS_DEF_FN, KEYWORD_IS_DEF_VAR, KEYWORD_PRINT, @@ -10,7 +10,8 @@ use crate::fn_call::run_builtin_binary_op; use crate::module::Module; use crate::parser::map_dynamic_to_expr; use crate::scope::Scope; -use crate::token::{is_valid_identifier, NO_POS}; +use crate::token::{is_valid_identifier, Position, NO_POS}; +use crate::utils::get_hasher; use crate::{calc_native_fn_hash, StaticVec}; #[cfg(not(feature = "no_function"))] @@ -18,7 +19,9 @@ use crate::ast::ReturnType; use crate::stdlib::{ boxed::Box, + hash::{Hash, Hasher}, iter::empty, + mem, string::{String, ToString}, vec, vec::Vec, @@ -154,285 +157,293 @@ fn call_fn_with_constant_arguments( .map(|(v, _)| v) } +/// Optimize a block of statements. +fn optimize_stmt_block( + mut statements: Vec, + pos: Position, + state: &mut State, + preserve_result: bool, + count_promote_as_dirty: bool, +) -> Stmt { + let orig_len = statements.len(); // Original number of statements in the block, for change detection + let orig_constants_len = state.constants.len(); // Original number of constants in the state, for restore later + + // Optimize each statement in the block + statements.iter_mut().for_each(|stmt| match stmt { + // Add constant literals into the state + Stmt::Const(var_def, Some(expr), _, pos) if expr.is_constant() => { + state.set_dirty(); + state.push_constant(&var_def.name, mem::take(expr)); + *stmt = Stmt::Noop(*pos); // No need to keep constants + } + Stmt::Const(var_def, None, _, pos) => { + state.set_dirty(); + state.push_constant(&var_def.name, Expr::Unit(var_def.pos)); + *stmt = Stmt::Noop(*pos); // No need to keep constants + } + // Optimize the statement + _ => optimize_stmt(stmt, state, preserve_result), + }); + + // Remove all raw expression statements that are pure except for the very last statement + let last_stmt = if preserve_result { + statements.pop() + } else { + None + }; + + statements.retain(|stmt| !stmt.is_pure()); + + if let Some(stmt) = last_stmt { + statements.push(stmt); + } + + // Remove all let/import statements at the end of a block - the new variables will go away anyway. + // But be careful only remove ones that have no initial values or have values that are pure expressions, + // otherwise there may be side effects. + let mut removed = false; + + while let Some(expr) = statements.pop() { + match expr { + Stmt::Let(_, expr, _, _) => removed = expr.as_ref().map(Expr::is_pure).unwrap_or(true), + #[cfg(not(feature = "no_module"))] + Stmt::Import(expr, _, _) => removed = expr.is_pure(), + _ => { + statements.push(expr); + break; + } + } + } + + if preserve_result { + if removed { + statements.push(Stmt::Noop(pos)) + } + + // Optimize all the statements again + let num_statements = statements.len(); + statements + .iter_mut() + .enumerate() + .for_each(|(i, stmt)| optimize_stmt(stmt, state, i == num_statements)); + } + + // Remove everything following the the first return/throw + let mut dead_code = false; + + statements.retain(|stmt| { + if dead_code { + return false; + } + + match stmt { + Stmt::ReturnWithVal(_, _, _) | Stmt::Break(_) => dead_code = true, + _ => (), + } + + true + }); + + // Change detection + if orig_len != statements.len() { + state.set_dirty(); + } + + // Pop the stack and remove all the local constants + state.restore_constants(orig_constants_len); + + match &statements[..] { + // No statements in block - change to No-op + [] => { + state.set_dirty(); + Stmt::Noop(pos) + } + // Only one let statement - leave it alone + [x] if matches!(x, Stmt::Let(_, _, _, _)) => Stmt::Block(statements, pos), + // Only one import statement - leave it alone + #[cfg(not(feature = "no_module"))] + [x] if matches!(x, Stmt::Import(_, _, _)) => Stmt::Block(statements, pos), + // Only one statement - promote + [_] => { + if count_promote_as_dirty { + state.set_dirty(); + } + statements.remove(0) + } + _ => Stmt::Block(statements, pos), + } +} + /// Optimize a statement. -fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { +fn optimize_stmt(stmt: &mut Stmt, state: &mut State, preserve_result: bool) { match stmt { // expr op= expr - Stmt::Assignment(x, pos) => match x.0 { - Expr::Variable(_) => { - Stmt::Assignment(Box::new((x.0, x.1, optimize_expr(x.2, state))), pos) + Stmt::Assignment(ref mut x, _) => match x.0 { + Expr::Variable(_) => optimize_expr(&mut x.2, state), + _ => { + optimize_expr(&mut x.0, state); + optimize_expr(&mut x.2, state); } - _ => Stmt::Assignment( - Box::new((optimize_expr(x.0, state), x.1, optimize_expr(x.2, state))), - pos, - ), }, // if false { if_block } -> Noop Stmt::IfThenElse(Expr::False(pos), x, _) if x.1.is_none() => { state.set_dirty(); - Stmt::Noop(pos) + *stmt = Stmt::Noop(*pos); } // if true { if_block } -> if_block - Stmt::IfThenElse(Expr::True(_), x, _) if x.1.is_none() => optimize_stmt(x.0, state, true), + Stmt::IfThenElse(Expr::True(_), x, _) if x.1.is_none() => { + *stmt = mem::take(&mut x.0); + optimize_stmt(stmt, state, true); + } // if expr { Noop } - Stmt::IfThenElse(condition, x, _) if x.1.is_none() && matches!(x.0, Stmt::Noop(_)) => { + Stmt::IfThenElse(ref mut condition, x, _) + if x.1.is_none() && matches!(x.0, Stmt::Noop(_)) => + { state.set_dirty(); let pos = condition.position(); - let expr = optimize_expr(condition, state); + let mut expr = mem::take(condition); + optimize_expr(&mut expr, state); - if preserve_result { + *stmt = if preserve_result { // -> { expr, Noop } let mut statements = Vec::new(); statements.push(Stmt::Expr(expr)); - statements.push(x.0); - + statements.push(mem::take(&mut x.0)); Stmt::Block(statements, pos) } else { // -> expr Stmt::Expr(expr) - } + }; } // if expr { if_block } - Stmt::IfThenElse(condition, x, pos) if x.1.is_none() => Stmt::IfThenElse( - optimize_expr(condition, state), - Box::new((optimize_stmt(x.0, state, true), None)), - pos, - ), + Stmt::IfThenElse(ref mut condition, ref mut x, _) if x.1.is_none() => { + optimize_expr(condition, state); + optimize_stmt(&mut x.0, state, true); + } // if false { if_block } else { else_block } -> else_block Stmt::IfThenElse(Expr::False(_), x, _) if x.1.is_some() => { - optimize_stmt(x.1.unwrap(), state, true) + *stmt = mem::take(x.1.as_mut().unwrap()); + optimize_stmt(stmt, state, true); } // if true { if_block } else { else_block } -> if_block - Stmt::IfThenElse(Expr::True(_), x, _) => optimize_stmt(x.0, state, true), + Stmt::IfThenElse(Expr::True(_), x, _) => { + *stmt = mem::take(&mut x.0); + optimize_stmt(stmt, state, true); + } // if expr { if_block } else { else_block } - Stmt::IfThenElse(condition, x, pos) => Stmt::IfThenElse( - optimize_expr(condition, state), - Box::new(( - optimize_stmt(x.0, state, true), - match optimize_stmt(x.1.unwrap(), state, true) { - Stmt::Noop(_) => None, // Noop -> no else block - stmt => Some(stmt), - }, - )), - pos, - ), + Stmt::IfThenElse(ref mut condition, ref mut x, _) => { + optimize_expr(condition, state); + optimize_stmt(&mut x.0, state, true); + if let Some(else_block) = x.1.as_mut() { + optimize_stmt(else_block, state, true); + match else_block { + Stmt::Noop(_) => x.1 = None, // Noop -> no else block + _ => (), + } + } + } // while false { block } -> Noop Stmt::While(Expr::False(pos), _, _) => { state.set_dirty(); - Stmt::Noop(pos) + *stmt = Stmt::Noop(*pos) } // while true { block } -> loop { block } Stmt::While(Expr::True(_), block, pos) => { - Stmt::Loop(Box::new(optimize_stmt(*block, state, false)), pos) + optimize_stmt(block, state, false); + *stmt = Stmt::Loop(Box::new(mem::take(block)), *pos) } // while expr { block } - Stmt::While(condition, block, pos) => { - match optimize_stmt(*block, state, false) { + Stmt::While(condition, block, _) => { + optimize_stmt(block, state, false); + optimize_expr(condition, state); + + match **block { // while expr { break; } -> { expr; } Stmt::Break(pos) => { // Only a single break statement - turn into running the guard expression once state.set_dirty(); let mut statements = Vec::new(); - statements.push(Stmt::Expr(optimize_expr(condition, state))); + statements.push(Stmt::Expr(mem::take(condition))); if preserve_result { statements.push(Stmt::Noop(pos)) } - Stmt::Block(statements, pos) + *stmt = Stmt::Block(statements, pos); } - // while expr { block } - stmt => Stmt::While(optimize_expr(condition, state), Box::new(stmt), pos), + _ => (), } } // loop { block } - Stmt::Loop(block, pos) => match optimize_stmt(*block, state, false) { - // loop { break; } -> Noop - Stmt::Break(pos) => { - // Only a single break statement - state.set_dirty(); - Stmt::Noop(pos) + Stmt::Loop(block, _) => { + optimize_stmt(block, state, false); + + match **block { + // loop { break; } -> Noop + Stmt::Break(pos) => { + // Only a single break statement + state.set_dirty(); + *stmt = Stmt::Noop(pos) + } + _ => (), } - // loop { block } - stmt => Stmt::Loop(Box::new(stmt), pos), - }, + } // for id in expr { block } - Stmt::For(iterable, x, pos) => { - let (var_name, block) = *x; - Stmt::For( - optimize_expr(iterable, state), - Box::new((var_name, optimize_stmt(block, state, false))), - pos, - ) + Stmt::For(ref mut iterable, ref mut x, _) => { + optimize_expr(iterable, state); + optimize_stmt(&mut x.1, state, false); } // let id = expr; - Stmt::Let(name, Some(expr), export, pos) => { - Stmt::Let(name, Some(optimize_expr(expr, state)), export, pos) - } + Stmt::Let(_, Some(ref mut expr), _, _) => optimize_expr(expr, state), // let id; - stmt @ Stmt::Let(_, None, _, _) => stmt, + Stmt::Let(_, None, _, _) => (), // import expr as var; #[cfg(not(feature = "no_module"))] - Stmt::Import(expr, alias, pos) => Stmt::Import(optimize_expr(expr, state), alias, pos), + Stmt::Import(ref mut expr, _, _) => optimize_expr(expr, state), // { block } Stmt::Block(statements, pos) => { - let orig_len = statements.len(); // Original number of statements in the block, for change detection - let orig_constants_len = state.constants.len(); // Original number of constants in the state, for restore later - - // Optimize each statement in the block - let mut result: Vec<_> = statements - .into_iter() - .map(|stmt| match stmt { - // Add constant literals into the state - Stmt::Const(var_def, Some(expr), _, pos) if expr.is_literal() => { - state.set_dirty(); - state.push_constant(&var_def.name, expr); - Stmt::Noop(pos) // No need to keep constants - } - Stmt::Const(var_def, None, _, pos) => { - state.set_dirty(); - state.push_constant(&var_def.name, Expr::Unit(var_def.pos)); - Stmt::Noop(pos) // No need to keep constants - } - // Optimize the statement - stmt => optimize_stmt(stmt, state, preserve_result), - }) - .collect(); - - // Remove all raw expression statements that are pure except for the very last statement - let last_stmt = if preserve_result { result.pop() } else { None }; - - result.retain(|stmt| !stmt.is_pure()); - - if let Some(stmt) = last_stmt { - result.push(stmt); - } - - // Remove all let/import statements at the end of a block - the new variables will go away anyway. - // But be careful only remove ones that have no initial values or have values that are pure expressions, - // otherwise there may be side effects. - let mut removed = false; - - while let Some(expr) = result.pop() { - match expr { - Stmt::Let(_, expr, _, _) => { - removed = expr.as_ref().map(Expr::is_pure).unwrap_or(true) - } - #[cfg(not(feature = "no_module"))] - Stmt::Import(expr, _, _) => removed = expr.is_pure(), - _ => { - result.push(expr); - break; - } - } - } - - if preserve_result { - if removed { - result.push(Stmt::Noop(pos)) - } - - // Optimize all the statements again - result = result - .into_iter() - .rev() - .enumerate() - .map(|(i, stmt)| optimize_stmt(stmt, state, i == 0)) - .rev() - .collect(); - } - - // Remove everything following the the first return/throw - let mut dead_code = false; - - result.retain(|stmt| { - if dead_code { - return false; - } - - match stmt { - Stmt::ReturnWithVal(_, _, _) | Stmt::Break(_) => dead_code = true, - _ => (), - } - - true - }); - - // Change detection - if orig_len != result.len() { - state.set_dirty(); - } - - // Pop the stack and remove all the local constants - state.restore_constants(orig_constants_len); - - match &result[..] { - // No statements in block - change to No-op - [] => { - state.set_dirty(); - Stmt::Noop(pos) - } - // Only one let statement - leave it alone - [x] if matches!(x, Stmt::Let(_, _, _, _)) => Stmt::Block(result, pos), - // Only one import statement - leave it alone - #[cfg(not(feature = "no_module"))] - [x] if matches!(x, Stmt::Import(_, _, _)) => Stmt::Block(result, pos), - // Only one statement - promote - [_] => { - state.set_dirty(); - result.remove(0) - } - _ => Stmt::Block(result, pos), - } + *stmt = optimize_stmt_block(mem::take(statements), *pos, state, preserve_result, true); } // try { block } catch ( var ) { block } Stmt::TryCatch(x, _, _) if x.0.is_pure() => { // If try block is pure, there will never be any exceptions state.set_dirty(); let pos = x.0.position(); - let mut statements = match optimize_stmt(x.0, state, preserve_result) { + optimize_stmt(&mut x.0, state, preserve_result); + let mut statements = match mem::take(&mut x.0) { Stmt::Block(statements, _) => statements, stmt => vec![stmt], }; statements.push(Stmt::Noop(pos)); - Stmt::Block(statements, pos) + *stmt = Stmt::Block(statements, pos); } // try { block } catch ( var ) { block } - Stmt::TryCatch(x, try_pos, catch_pos) => { - let (try_block, var_name, catch_block) = *x; - Stmt::TryCatch( - Box::new(( - optimize_stmt(try_block, state, false), - var_name, - optimize_stmt(catch_block, state, false), - )), - try_pos, - catch_pos, - ) + Stmt::TryCatch(ref mut x, _, _) => { + optimize_stmt(&mut x.0, state, false); + optimize_stmt(&mut x.2, state, false); } // {} Stmt::Expr(Expr::Stmt(x, pos)) if x.is_empty() => { state.set_dirty(); - Stmt::Noop(pos) + *stmt = Stmt::Noop(*pos); } // {...}; Stmt::Expr(Expr::Stmt(x, pos)) => { state.set_dirty(); - Stmt::Block(x.into_vec(), pos) + *stmt = Stmt::Block(mem::take(x).into_vec(), *pos); } // expr; - Stmt::Expr(expr) => Stmt::Expr(optimize_expr(expr, state)), + Stmt::Expr(ref mut expr) => optimize_expr(expr, state), // return expr; - Stmt::ReturnWithVal(ret, Some(expr), pos) => { - Stmt::ReturnWithVal(ret, Some(optimize_expr(expr, state)), pos) - } + Stmt::ReturnWithVal(_, Some(ref mut expr), _) => optimize_expr(expr, state), + // All other statements - skip - stmt => stmt, + _ => (), } } /// Optimize an expression. -fn optimize_expr(expr: Expr, state: &mut State) -> Expr { +fn optimize_expr(expr: &mut Expr, state: &mut State) { // These keywords are handled specially const DONT_EVAL_KEYWORDS: &[&str] = &[ KEYWORD_PRINT, // side effects @@ -444,296 +455,289 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { match expr { // expr - do not promote because there is a reason it is wrapped in an `Expr::Expr` - Expr::Expr(x) => Expr::Expr(Box::new(optimize_expr(*x, state))), + Expr::Expr(x) => optimize_expr(x, state), // {} - Expr::Stmt(x, pos) if x.is_empty() => { - state.set_dirty(); - Expr::Unit(pos) - } - // { stmt } - Expr::Stmt(mut x, pos) if x.len() == 1 => match x.pop().unwrap() { - // {} -> () - Stmt::Noop(_) => { - state.set_dirty(); - Expr::Unit(pos) - } - // { expr } -> expr - Stmt::Expr(expr) => { - state.set_dirty(); - optimize_expr(expr, state) - } + Expr::Stmt(x, pos) if x.is_empty() => { state.set_dirty(); *expr = Expr::Unit(*pos) } + // { stmt; ... } - do not count promotion as dirty because it gets turned back into an array + Expr::Stmt(x, pos) => match optimize_stmt_block(mem::take(x).into_vec(), *pos, state, true, false) { + // {} + Stmt::Noop(_) => { state.set_dirty(); *expr = Expr::Unit(*pos); } + // { stmt, .. } + Stmt::Block(statements, _) => *x = Box::new(statements.into()), + // { expr } + Stmt::Expr(inner) => { state.set_dirty(); *expr = inner; } // { stmt } - stmt => Expr::Stmt(Box::new(vec![optimize_stmt(stmt, state, true)].into()), pos) + stmt => x.push(stmt), } - // { stmt; ... } - Expr::Stmt(x, pos) => Expr::Stmt(Box::new( - x.into_iter().map(|stmt| optimize_stmt(stmt, state, true)).collect(), - ), pos), // lhs.rhs #[cfg(not(feature = "no_object"))] - Expr::Dot(x, dot_pos) => match (x.lhs, x.rhs) { + Expr::Dot(x, _) => match (&mut x.lhs, &mut x.rhs) { // map.string (Expr::Map(m, pos), Expr::Property(p)) if m.iter().all(|(_, x)| x.is_pure()) => { let prop = &p.1.name; // Map literal where everything is pure - promote the indexed item. // All other items can be thrown away. state.set_dirty(); - m.into_iter().find(|(x, _)| &x.name == prop) - .map(|(_, mut expr)| { expr.set_position(pos); expr }) - .unwrap_or_else(|| Expr::Unit(pos)) + *expr = mem::take(m).into_iter().find(|(x, _)| &x.name == prop) + .map(|(_, mut expr)| { expr.set_position(*pos); expr }) + .unwrap_or_else(|| Expr::Unit(*pos)); } // var.rhs - (lhs @ Expr::Variable(_), rhs) => Expr::Dot(Box::new(BinaryExpr { - lhs, - rhs: optimize_expr(rhs, state), - }), dot_pos), + (Expr::Variable(_), rhs) => optimize_expr(rhs, state), // lhs.rhs - (lhs, rhs) => Expr::Dot(Box::new(BinaryExpr { - lhs: optimize_expr(lhs, state), - rhs: optimize_expr(rhs, state), - }), dot_pos) + (lhs, rhs) => { optimize_expr(lhs, state); optimize_expr(rhs, state); } } // lhs[rhs] #[cfg(not(feature = "no_index"))] - Expr::Index(x, idx_pos) => match (x.lhs, x.rhs) { + Expr::Index(x, _) => match (&mut x.lhs, &mut x.rhs) { // array[int] - (Expr::Array(mut a, pos), Expr::IntegerConstant(i, _)) - if i >= 0 && (i as usize) < a.len() && a.iter().all(Expr::is_pure) => + (Expr::Array(a, pos), Expr::IntegerConstant(i, _)) + if *i >= 0 && (*i as usize) < a.len() && a.iter().all(Expr::is_pure) => { // Array literal where everything is pure - promote the indexed item. // All other items can be thrown away. state.set_dirty(); - let mut expr = a.remove(i as usize); - expr.set_position(pos); - expr + let mut result = a.remove(*i as usize); + result.set_position(*pos); + *expr = result; } // map[string] - (Expr::Map(m, pos), Expr::StringConstant(s)) if m.iter().all(|(_, x)| x.is_pure()) => { + (Expr::Map(m, pos), Expr::StringConstant(s, _)) if m.iter().all(|(_, x)| x.is_pure()) => { // Map literal where everything is pure - promote the indexed item. // All other items can be thrown away. state.set_dirty(); - m.into_iter().find(|(x, _)| x.name == s.name) - .map(|(_, mut expr)| { expr.set_position(pos); expr }) - .unwrap_or_else(|| Expr::Unit(pos)) + *expr = mem::take(m).into_iter().find(|(x, _)| x.name == *s) + .map(|(_, mut expr)| { expr.set_position(*pos); expr }) + .unwrap_or_else(|| Expr::Unit(*pos)); } // string[int] - (Expr::StringConstant(s), Expr::IntegerConstant(i, _)) if i >= 0 && (i as usize) < s.name.chars().count() => { + (Expr::StringConstant(s, pos), Expr::IntegerConstant(i, _)) if *i >= 0 && (*i as usize) < s.chars().count() => { // String literal indexing - get the character state.set_dirty(); - Expr::CharConstant(s.name.chars().nth(i as usize).unwrap(), s.pos) + *expr = Expr::CharConstant(s.chars().nth(*i as usize).unwrap(), *pos); } // var[rhs] - (lhs @ Expr::Variable(_), rhs) => Expr::Index(Box::new(BinaryExpr { - lhs, - rhs: optimize_expr(rhs, state), - }), idx_pos), + (Expr::Variable(_), rhs) => optimize_expr(rhs, state), // lhs[rhs] - (lhs, rhs) => Expr::Index(Box::new(BinaryExpr { - lhs: optimize_expr(lhs, state), - rhs: optimize_expr(rhs, state), - }), idx_pos), + (lhs, rhs) => { optimize_expr(lhs, state); optimize_expr(rhs, state); } }, // [ items .. ] #[cfg(not(feature = "no_index"))] - Expr::Array(a, pos) => Expr::Array(Box::new(a - .into_iter().map(|expr| optimize_expr(expr, state)) - .collect()), pos), - // [ items .. ] + Expr::Array(a, _) => a.iter_mut().for_each(|expr| optimize_expr(expr, state)), + // #{ key:value, .. } #[cfg(not(feature = "no_object"))] - Expr::Map(m, pos) => Expr::Map(Box::new(m - .into_iter().map(|(key, expr)| (key, optimize_expr(expr, state))) - .collect()), pos), + Expr::Map(m, _) => m.iter_mut().for_each(|(_, expr)| optimize_expr(expr, state)), // lhs in rhs - Expr::In(x, in_pos) => match (x.lhs, x.rhs) { + Expr::In(x, _) => match (&mut x.lhs, &mut x.rhs) { // "xxx" in "xxxxx" - (Expr::StringConstant(a), Expr::StringConstant(b)) => { + (Expr::StringConstant(a, pos), Expr::StringConstant(b, _)) => { state.set_dirty(); - if b.name.contains(a.name.as_str()) { Expr::True(a.pos) } else { Expr::False(a.pos) } + *expr = if b.contains(a.as_str()) { Expr::True(*pos) } else { Expr::False(*pos) }; } // 'x' in "xxxxx" - (Expr::CharConstant(a, pos), Expr::StringConstant(b)) => { + (Expr::CharConstant(a, pos), Expr::StringConstant(b, _)) => { state.set_dirty(); - if b.name.contains(a) { Expr::True(pos) } else { Expr::False(pos) } + *expr = if b.contains(*a) { Expr::True(*pos) } else { Expr::False(*pos) }; } // "xxx" in #{...} - (Expr::StringConstant(a), Expr::Map(b, _)) => { + (Expr::StringConstant(a, pos), Expr::Map(b, _)) => { state.set_dirty(); - if b.iter().find(|(x, _)| x.name == a.name).is_some() { - Expr::True(a.pos) + *expr = if b.iter().find(|(x, _)| x.name == *a).is_some() { + Expr::True(*pos) } else { - Expr::False(a.pos) - } + Expr::False(*pos) + }; } // 'x' in #{...} (Expr::CharConstant(a, pos), Expr::Map(b, _)) => { state.set_dirty(); let ch = a.to_string(); - if b.iter().find(|(x, _)| x.name == &ch).is_some() { - Expr::True(pos) + *expr = if b.iter().find(|(x, _)| x.name == &ch).is_some() { + Expr::True(*pos) } else { - Expr::False(pos) - } + Expr::False(*pos) + }; } // lhs in rhs - (lhs, rhs) => Expr::In(Box::new(BinaryExpr { - lhs: optimize_expr(lhs, state), - rhs: optimize_expr(rhs, state), - }), in_pos), + (lhs, rhs) => { optimize_expr(lhs, state); optimize_expr(rhs, state); } }, // lhs && rhs - Expr::And(x, and_pos) => match (x.lhs, x.rhs) { + Expr::And(x, _) => match (&mut x.lhs, &mut x.rhs) { // true && rhs -> rhs (Expr::True(_), rhs) => { state.set_dirty(); - rhs + optimize_expr(rhs, state); + *expr = mem::take(rhs); } // false && rhs -> false (Expr::False(pos), _) => { state.set_dirty(); - Expr::False(pos) + *expr = Expr::False(*pos); } // lhs && true -> lhs (lhs, Expr::True(_)) => { state.set_dirty(); - optimize_expr(lhs, state) + optimize_expr(lhs, state); + *expr = mem::take(lhs); } // lhs && rhs - (lhs, rhs) => Expr::And(Box::new(BinaryExpr { - lhs: optimize_expr(lhs, state), - rhs: optimize_expr(rhs, state), - }), and_pos), + (lhs, rhs) => { optimize_expr(lhs, state); optimize_expr(rhs, state); } }, // lhs || rhs - Expr::Or(x, or_pos) => match (x.lhs, x.rhs) { + Expr::Or(ref mut x, _) => match (&mut x.lhs, &mut x.rhs) { // false || rhs -> rhs (Expr::False(_), rhs) => { state.set_dirty(); - rhs + optimize_expr(rhs, state); + *expr = mem::take(rhs); } // true || rhs -> true (Expr::True(pos), _) => { state.set_dirty(); - Expr::True(pos) + *expr = Expr::True(*pos); } // lhs || false (lhs, Expr::False(_)) => { state.set_dirty(); - optimize_expr(lhs, state) + optimize_expr(lhs, state); + *expr = mem::take(lhs); } // lhs || rhs - (lhs, rhs) => Expr::Or(Box::new(BinaryExpr { - lhs: optimize_expr(lhs, state), - rhs: optimize_expr(rhs, state), - }), or_pos), + (lhs, rhs) => { optimize_expr(lhs, state); optimize_expr(rhs, state); } }, // Do not call some special keywords - Expr::FnCall(mut x, pos) if DONT_EVAL_KEYWORDS.contains(&x.name.as_ref()) => { - x.args = x.args.into_iter().map(|a| optimize_expr(a, state)).collect(); - Expr::FnCall(x, pos) + Expr::FnCall(x, _) if DONT_EVAL_KEYWORDS.contains(&x.name.as_ref()) => { + x.args.iter_mut().for_each(|a| optimize_expr(a, state)); } // Call built-in operators - Expr::FnCall(mut x, pos) + Expr::FnCall(x, pos) if x.namespace.is_none() // Non-qualified && state.optimization_level == OptimizationLevel::Simple // simple optimizations && x.args.len() == 2 // binary call && x.args.iter().all(Expr::is_constant) // all arguments are constants && !is_valid_identifier(x.name.chars()) // cannot be scripted => { - let FnCallInfo { name, args, .. } = x.as_mut(); - - let arg_values: StaticVec<_> = args.iter().map(|e| e.get_constant_value().unwrap()).collect(); + let arg_values: StaticVec<_> = x.args.iter().map(|e| e.get_constant_value().unwrap()).collect(); let arg_types: StaticVec<_> = arg_values.iter().map(Dynamic::type_id).collect(); // Search for overloaded operators (can override built-in). - if !state.engine.has_override_by_name_and_arguments(state.lib, name, arg_types.as_ref(), false) { - if let Some(expr) = run_builtin_binary_op(name, &arg_values[0], &arg_values[1]) + if !state.engine.has_override_by_name_and_arguments(state.lib, x.name.as_ref(), arg_types.as_ref(), false) { + if let Some(result) = run_builtin_binary_op(x.name.as_ref(), &arg_values[0], &arg_values[1]) .ok().flatten() - .and_then(|result| map_dynamic_to_expr(result, pos)) + .and_then(|result| map_dynamic_to_expr(result, *pos)) { state.set_dirty(); - return expr; + *expr = result; + return; } } - x.args = x.args.into_iter().map(|a| optimize_expr(a, state)).collect(); - Expr::FnCall(x, pos) + x.args.iter_mut().for_each(|a| optimize_expr(a, state)); } // Eagerly call functions - Expr::FnCall(mut x, pos) + Expr::FnCall(x, pos) if x.namespace.is_none() // Non-qualified && state.optimization_level == OptimizationLevel::Full // full optimizations && x.args.iter().all(Expr::is_constant) // all arguments are constants => { - let FnCallInfo { name, args, def_value, .. } = x.as_mut(); - // First search for script-defined functions (can override built-in) #[cfg(not(feature = "no_function"))] - let has_script_fn = state.lib.iter().any(|&m| m.get_script_fn(name, args.len(), false).is_some()); + let has_script_fn = state.lib.iter().any(|&m| m.get_script_fn(x.name.as_ref(), x.args.len(), false).is_some()); #[cfg(feature = "no_function")] let has_script_fn = false; if !has_script_fn { - let mut arg_values: StaticVec<_> = args.iter().map(|e| e.get_constant_value().unwrap()).collect(); + let mut arg_values: StaticVec<_> = x.args.iter().map(|e| e.get_constant_value().unwrap()).collect(); // Save the typename of the first argument if it is `type_of()` // This is to avoid `call_args` being passed into the closure - let arg_for_type_of = if name == KEYWORD_TYPE_OF && arg_values.len() == 1 { + let arg_for_type_of = if x.name == KEYWORD_TYPE_OF && arg_values.len() == 1 { state.engine.map_type_name(arg_values[0].type_name()) } else { "" }; - if let Some(expr) = call_fn_with_constant_arguments(&state, name, arg_values.as_mut()) + if let Some(result) = call_fn_with_constant_arguments(&state, x.name.as_ref(), arg_values.as_mut()) .or_else(|| { if !arg_for_type_of.is_empty() { // Handle `type_of()` Some(arg_for_type_of.to_string().into()) } else { // Otherwise use the default value, if any - def_value.map(|v| v.into()) + x.def_value.map(|v| v.into()) } }) - .and_then(|result| map_dynamic_to_expr(result, pos)) + .and_then(|result| map_dynamic_to_expr(result, *pos)) { state.set_dirty(); - return expr; + *expr = result; + return; } } - x.args = x.args.into_iter().map(|a| optimize_expr(a, state)).collect(); - Expr::FnCall(x, pos) + x.args.iter_mut().for_each(|a| optimize_expr(a, state)); } // id(args ..) -> optimize function call arguments - Expr::FnCall(mut x, pos) => { - x.args = x.args.into_iter().map(|a| optimize_expr(a, state)).collect(); - Expr::FnCall(x, pos) - } + Expr::FnCall(x, _) => x.args.iter_mut().for_each(|a| optimize_expr(a, state)), // constant-name Expr::Variable(x) if x.1.is_none() && state.contains_constant(&x.3.name) => { state.set_dirty(); // Replace constant with value - let mut expr = state.find_constant(&x.3.name).unwrap().clone(); - expr.set_position(x.3.pos); - expr + let mut result = state.find_constant(&x.3.name).unwrap().clone(); + result.set_position(x.3.pos); + *expr = result; + } + + // switch const { ... } + Expr::Switch(x, pos) if x.0.is_constant() => { + let value = x.0.get_constant_value().unwrap(); + let hasher = &mut get_hasher(); + value.hash(hasher); + let hash = hasher.finish(); + + state.set_dirty(); + + let table = &mut x.1; + + if let Some(stmt) = table.get_mut(&hash) { + optimize_stmt(stmt, state, true); + *expr = Expr::Stmt(Box::new(vec![mem::take(stmt)].into()), *pos); + } else if let Some(def_stmt) = x.2.as_mut() { + optimize_stmt(def_stmt, state, true); + *expr = Expr::Stmt(Box::new(vec![mem::take(def_stmt)].into()), *pos); + } else { + *expr = Expr::Unit(*pos); + } + } + + // switch + Expr::Switch(x, _) => { + optimize_expr(&mut x.0, state); + x.1.values_mut().for_each(|stmt| optimize_stmt(stmt, state, true)); + if let Some(def_stmt) = x.2.as_mut() { + optimize_stmt(def_stmt, state, true); + + match def_stmt { + Stmt::Noop(_) | Stmt::Expr(Expr::Unit(_)) => x.2 = None, + _ => () + } + } } // Custom syntax - Expr::Custom(x, pos) => Expr::Custom(Box::new(CustomExpr { - keywords: x.keywords.into_iter().map(|expr| optimize_expr(expr, state)).collect(), - ..*x - }), pos), + Expr::Custom(x, _) => x.keywords.iter_mut().for_each(|expr| optimize_expr(expr, state)), // All other expressions - skip - expr => expr, + _ => (), } } @@ -773,47 +777,39 @@ fn optimize( let num_statements = result.len(); - result = result - .into_iter() - .enumerate() - .map(|(i, stmt)| { - match stmt { - Stmt::Const(var_def, Some(expr), export, pos) => { - // Load constants - let expr = optimize_expr(expr, &mut state); + result.iter_mut().enumerate().for_each(|(i, stmt)| { + match stmt { + Stmt::Const(var_def, expr, _, _) if expr.is_some() => { + // Load constants + let value_expr = expr.as_mut().unwrap(); + optimize_expr(value_expr, &mut state); - if expr.is_literal() { - state.push_constant(&var_def.name, expr.clone()); - } - - // Keep it in the global scope - if expr.is_unit() { - state.set_dirty(); - Stmt::Const(var_def, None, export, pos) - } else { - Stmt::Const(var_def, Some(expr), export, pos) - } + if value_expr.is_constant() { + state.push_constant(&var_def.name, value_expr.clone()); } - Stmt::Const(ref var_def, None, _, _) => { - state.push_constant(&var_def.name, Expr::Unit(var_def.pos)); - // Keep it in the global scope - stmt - } - _ => { - // Keep all variable declarations at this level - // and always keep the last return value - let keep = match stmt { - Stmt::Let(_, _, _, _) => true, - #[cfg(not(feature = "no_module"))] - Stmt::Import(_, _, _) => true, - _ => i == num_statements - 1, - }; - optimize_stmt(stmt, &mut state, keep) + // Keep it in the global scope + if value_expr.is_unit() { + state.set_dirty(); + *expr = None; } } - }) - .collect(); + Stmt::Const(var_def, None, _, _) => { + state.push_constant(&var_def.name, Expr::Unit(var_def.pos)); + } + _ => { + // Keep all variable declarations at this level + // and always keep the last return value + let keep = match stmt { + Stmt::Let(_, _, _, _) => true, + #[cfg(not(feature = "no_module"))] + Stmt::Import(_, _, _) => true, + _ => i == num_statements - 1, + }; + optimize_stmt(stmt, &mut state, keep); + } + } + }); if !state.is_dirty() { break; diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index 63abbf48..339c4c94 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -281,7 +281,7 @@ mod array_functions { }) .map_err(|err| { Box::new(EvalAltResult::ErrorInFunctionCall( - "filter".to_string(), + "index_of".to_string(), err, NO_POS, )) @@ -314,7 +314,7 @@ mod array_functions { }) .map_err(|err| { Box::new(EvalAltResult::ErrorInFunctionCall( - "filter".to_string(), + "some".to_string(), err, NO_POS, )) @@ -347,7 +347,7 @@ mod array_functions { }) .map_err(|err| { Box::new(EvalAltResult::ErrorInFunctionCall( - "filter".to_string(), + "all".to_string(), err, NO_POS, )) @@ -449,7 +449,7 @@ mod array_functions { }) .map_err(|err| { Box::new(EvalAltResult::ErrorInFunctionCall( - "reduce".to_string(), + "reduce_rev".to_string(), err, NO_POS, )) @@ -467,7 +467,7 @@ mod array_functions { ) -> Result> { let mut result = initial.call_dynamic(ctx, None, []).map_err(|err| { Box::new(EvalAltResult::ErrorInFunctionCall( - "reduce".to_string(), + "reduce_rev".to_string(), err, NO_POS, )) @@ -486,7 +486,7 @@ mod array_functions { }) .map_err(|err| { Box::new(EvalAltResult::ErrorInFunctionCall( - "reduce".to_string(), + "reduce_rev".to_string(), err, NO_POS, )) @@ -556,7 +556,7 @@ mod array_functions { }) .map_err(|err| { Box::new(EvalAltResult::ErrorInFunctionCall( - "filter".to_string(), + "drain".to_string(), err, NO_POS, )) @@ -615,7 +615,7 @@ mod array_functions { }) .map_err(|err| { Box::new(EvalAltResult::ErrorInFunctionCall( - "filter".to_string(), + "retain".to_string(), err, NO_POS, )) diff --git a/src/parse_error.rs b/src/parse_error.rs index ee858927..f34a8f36 100644 --- a/src/parse_error.rs +++ b/src/parse_error.rs @@ -108,6 +108,8 @@ pub enum ParseErrorType { /// /// Never appears under the `no_object` feature. DuplicatedProperty(String), + /// A switch case is duplicated. + DuplicatedSwitchCase, /// Missing a property name for custom types and maps. /// /// Never appears under the `no_object` feature. @@ -177,6 +179,7 @@ impl ParseErrorType { Self::MalformedInExpr(_) => "Invalid 'in' expression", Self::MalformedCapture(_) => "Invalid capturing", Self::DuplicatedProperty(_) => "Duplicated property in object map literal", + Self::DuplicatedSwitchCase => "Duplicated switch case", Self::PropertyExpected => "Expecting name of a property", Self::VariableExpected => "Expecting name of a variable", Self::Reserved(_) => "Invalid use of reserved keyword", @@ -211,6 +214,7 @@ impl fmt::Display for ParseErrorType { Self::DuplicatedProperty(s) => { write!(f, "Duplicated property '{}' for object map literal", s) } + Self::DuplicatedSwitchCase => f.write_str(self.desc()), Self::ExprExpected(s) => write!(f, "Expecting {} expression", s), diff --git a/src/parser.rs b/src/parser.rs index daa73352..4d6091e3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,11 +1,11 @@ //! Main module defining the lexer and parser. use crate::ast::{ - BinaryExpr, CustomExpr, Expr, FnCallInfo, Ident, IdentX, ReturnType, ScriptFnDef, Stmt, AST, + BinaryExpr, CustomExpr, Expr, FnCallExpr, Ident, IdentX, ReturnType, ScriptFnDef, Stmt, AST, }; use crate::dynamic::{Dynamic, Union}; use crate::engine::{Engine, KEYWORD_THIS, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT}; -use crate::module::ModuleRef; +use crate::module::NamespaceRef; use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::parse_error::{LexError, ParseError, ParseErrorType}; use crate::scope::{EntryType as ScopeEntryType, Scope}; @@ -13,11 +13,11 @@ use crate::syntax::CustomSyntax; use crate::token::{ is_keyword_function, is_valid_identifier, Position, Token, TokenStream, NO_POS, }; -use crate::utils::StraightHasherBuilder; +use crate::utils::{get_hasher, ImmutableString, StraightHasherBuilder}; use crate::{calc_script_fn_hash, StaticVec}; #[cfg(not(feature = "no_float"))] -use crate::ast::FloatWrapper; +use crate::FLOAT; #[cfg(not(feature = "no_object"))] use crate::engine::{make_getter, make_setter, KEYWORD_EVAL, KEYWORD_FN_PTR}; @@ -26,7 +26,6 @@ use crate::engine::{make_getter, make_setter, KEYWORD_EVAL, KEYWORD_FN_PTR}; use crate::{ ast::FnAccess, engine::{FN_ANONYMOUS, KEYWORD_FN_PTR_CURRY}, - utils::ImmutableString, }; use crate::stdlib::{ @@ -34,7 +33,7 @@ use crate::stdlib::{ boxed::Box, collections::HashMap, format, - hash::Hash, + hash::{Hash, Hasher}, iter::empty, num::NonZeroUsize, string::{String, ToString}, @@ -42,33 +41,26 @@ use crate::stdlib::{ vec::Vec, }; -#[cfg(not(feature = "no_function"))] -use crate::stdlib::hash::Hasher; - #[cfg(not(feature = "no_closure"))] use crate::stdlib::collections::HashSet; -#[cfg(not(feature = "no_std"))] -#[cfg(not(feature = "no_function"))] -use crate::stdlib::collections::hash_map::DefaultHasher; - -#[cfg(feature = "no_std")] -#[cfg(not(feature = "no_function"))] -use ahash::AHasher; - type PERR = ParseErrorType; type FunctionsLib = HashMap; -#[derive(Clone)] +#[derive(Debug)] struct ParseState<'e> { /// Reference to the scripting `Engine`. engine: &'e Engine, + /// Hash that uniquely identifies a script. + script_hash: u64, + /// Interned strings. + strings: HashMap, /// Encapsulates a local stack with variable names to simulate an actual runtime scope. - stack: Vec<(String, ScopeEntryType)>, + stack: Vec<(ImmutableString, ScopeEntryType)>, /// Tracks a list of external variables (variables that are not explicitly declared in the scope). #[cfg(not(feature = "no_closure"))] - externals: HashMap, + externals: HashMap, /// An indicator that disables variable capturing into externals one single time /// up until the nearest consumed Identifier token. /// If set to false the next call to `access_var` will not capture the variable. @@ -77,7 +69,7 @@ struct ParseState<'e> { allow_capture: bool, /// Encapsulates a local stack with imported module names. #[cfg(not(feature = "no_module"))] - modules: Vec, + modules: Vec, /// Maximum levels of expression nesting. #[cfg(not(feature = "unchecked"))] max_expr_depth: usize, @@ -92,6 +84,7 @@ impl<'e> ParseState<'e> { #[inline(always)] pub fn new( engine: &'e Engine, + script_hash: u64, #[cfg(not(feature = "unchecked"))] max_expr_depth: usize, #[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "no_function"))] @@ -99,6 +92,7 @@ impl<'e> ParseState<'e> { ) -> Self { Self { engine, + script_hash, #[cfg(not(feature = "unchecked"))] max_expr_depth, #[cfg(not(feature = "unchecked"))] @@ -108,6 +102,7 @@ impl<'e> ParseState<'e> { externals: Default::default(), #[cfg(not(feature = "no_closure"))] allow_capture: true, + strings: Default::default(), stack: Default::default(), #[cfg(not(feature = "no_module"))] modules: Default::default(), @@ -134,7 +129,7 @@ impl<'e> ParseState<'e> { #[cfg(not(feature = "no_closure"))] if self.allow_capture { if index.is_none() && !self.externals.contains_key(name) { - self.externals.insert(name.to_string(), _pos); + self.externals.insert(name.into(), _pos); } } else { self.allow_capture = true @@ -160,9 +155,26 @@ impl<'e> ParseState<'e> { .iter() .rev() .enumerate() - .find(|(_, n)| *n == name) + .find(|(_, n)| **n == name) .and_then(|(i, _)| NonZeroUsize::new(i + 1)) } + + /// Get an interned string, creating one if it is not yet interned. + pub fn get_interned_string(&mut self, text: S) -> ImmutableString + where + S: AsRef + Into, + { + #[allow(clippy::map_entry)] + if !self.strings.contains_key(text.as_ref()) { + let value: ImmutableString = text.into(); + let result = value.clone(); + let key = value.to_string(); + self.strings.insert(key, value); + result + } else { + self.strings.get(text.as_ref()).unwrap().clone() + } + } } #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] @@ -208,6 +220,24 @@ impl ParseSettings { } } } + +impl Expr { + /// Convert a `Variable` into a `Property`. All other variants are untouched. + #[cfg(not(feature = "no_object"))] + #[inline] + fn into_property(self, state: &mut ParseState) -> Self { + match self { + Self::Variable(x) if x.1.is_none() => { + let ident = x.3; + let getter = state.get_interned_string(make_getter(&ident.name)); + let setter = state.get_interned_string(make_setter(&ident.name)); + Self::Property(Box::new(((getter, setter), ident.into()))) + } + _ => self, + } + } +} + /// Consume a particular token, checking that it is the expected one. fn eat_token(input: &mut TokenStream, token: Token) -> Position { let (t, pos) = input.next().unwrap(); @@ -268,9 +298,9 @@ fn parse_fn_call( input: &mut TokenStream, state: &mut ParseState, lib: &mut FunctionsLib, - id: String, + id: ImmutableString, capture: bool, - mut namespace: Option>, + mut namespace: Option>, settings: ParseSettings, ) -> Result { let (token, token_pos) = input.peek().unwrap(); @@ -313,8 +343,8 @@ fn parse_fn_call( }; return Ok(Expr::FnCall( - Box::new(FnCallInfo { - name: id.into(), + Box::new(FnCallExpr { + name: id.to_string().into(), capture, namespace, hash: hash_script, @@ -360,8 +390,8 @@ fn parse_fn_call( }; return Ok(Expr::FnCall( - Box::new(FnCallInfo { - name: id.into(), + Box::new(FnCallExpr { + name: id.to_string().into(), capture, namespace, hash: hash_script, @@ -423,7 +453,7 @@ fn parse_index_chain( .into_err(*pos)) } Expr::IntegerConstant(_, pos) => match lhs { - Expr::Array(_, _) | Expr::StringConstant(_) => (), + Expr::Array(_, _) | Expr::StringConstant(_, _) => (), Expr::Map(_, _) => { return Err(PERR::MalformedIndexExpr( @@ -457,14 +487,14 @@ fn parse_index_chain( }, // lhs[string] - Expr::StringConstant(x) => match lhs { + Expr::StringConstant(_, pos) => match lhs { Expr::Map(_, _) => (), - Expr::Array(_, _) | Expr::StringConstant(_) => { + Expr::Array(_, _) | Expr::StringConstant(_, _) => { return Err(PERR::MalformedIndexExpr( "Array or string expects numeric index, not a string".into(), ) - .into_err(x.pos)) + .into_err(*pos)) } #[cfg(not(feature = "no_float"))] @@ -596,7 +626,9 @@ fn parse_array_literal( let mut arr = StaticVec::new(); - while !input.peek().unwrap().0.is_eof() { + loop { + const MISSING_RBRACKET: &str = "to end this array literal"; + #[cfg(not(feature = "unchecked"))] if state.engine.max_array_size() > 0 && arr.len() >= state.engine.max_array_size() { return Err(PERR::LiteralTooLarge( @@ -611,6 +643,12 @@ fn parse_array_literal( eat_token(input, Token::RightBracket); break; } + (Token::EOF, pos) => { + return Err( + PERR::MissingToken(Token::RightBracket.into(), MISSING_RBRACKET.into()) + .into_err(*pos), + ) + } _ => { let expr = parse_expr(input, state, lib, settings.level_up())?; arr.push(expr); @@ -623,11 +661,10 @@ fn parse_array_literal( } (Token::RightBracket, _) => (), (Token::EOF, pos) => { - return Err(PERR::MissingToken( - Token::RightBracket.into(), - "to end this array literal".into(), + return Err( + PERR::MissingToken(Token::RightBracket.into(), MISSING_RBRACKET.into()) + .into_err(*pos), ) - .into_err(*pos)) } (Token::LexError(err), pos) => return Err(err.into_err(*pos)), (_, pos) => { @@ -654,9 +691,9 @@ fn parse_map_literal( #[cfg(not(feature = "unchecked"))] settings.ensure_level_within_max_limit(state.max_expr_depth)?; - let mut map = StaticVec::new(); + let mut map: StaticVec<(IdentX, Expr)> = Default::default(); - while !input.peek().unwrap().0.is_eof() { + loop { const MISSING_RBRACE: &str = "to end this object map literal"; match input.peek().unwrap() { @@ -664,12 +701,22 @@ fn parse_map_literal( eat_token(input, Token::RightBrace); break; } + (Token::EOF, pos) => { + return Err( + PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) + .into_err(*pos), + ) + } _ => (), } let (name, pos) = match input.next().unwrap() { - (Token::Identifier(s), pos) => (s, pos), - (Token::StringConstant(s), pos) => (s, pos), + (Token::Identifier(s), pos) | (Token::StringConstant(s), pos) => { + if map.iter().any(|(p, _)| p.name == &s) { + return Err(PERR::DuplicatedProperty(s).into_err(pos)); + } + (s, pos) + } (Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => { return Err(PERR::Reserved(s).into_err(pos)); } @@ -738,20 +785,128 @@ fn parse_map_literal( } } - // Check for duplicating properties - map.iter() - .enumerate() - .try_for_each(|(i, (IdentX { name: k1, .. }, _))| { - map.iter() - .skip(i + 1) - .find(|(IdentX { name: k2, .. }, _)| k2 == k1) - .map_or_else(|| Ok(()), |(IdentX { name: k2, pos }, _)| Err((k2, *pos))) - }) - .map_err(|(key, pos)| PERR::DuplicatedProperty(key.to_string()).into_err(pos))?; - Ok(Expr::Map(Box::new(map), settings.pos)) } +/// Parse a switch expression. +fn parse_switch( + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FunctionsLib, + settings: ParseSettings, +) -> Result { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + let item = parse_expr(input, state, lib, settings.level_up())?; + + match input.next().unwrap() { + (Token::LeftBrace, _) => (), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (_, pos) => { + return Err(PERR::MissingToken( + Token::LeftBrace.into(), + "to start a switch block".into(), + ) + .into_err(pos)) + } + } + + let mut table: HashMap = Default::default(); + let mut def_stmt = None; + + loop { + const MISSING_RBRACE: &str = "to end this switch block"; + + let expr = match input.peek().unwrap() { + (Token::RightBrace, _) => { + eat_token(input, Token::RightBrace); + break; + } + (Token::EOF, pos) => { + return Err( + PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) + .into_err(*pos), + ) + } + (Token::Underscore, _) if def_stmt.is_none() => { + eat_token(input, Token::Underscore); + None + } + (Token::Underscore, pos) => return Err(PERR::DuplicatedSwitchCase.into_err(*pos)), + _ => Some(parse_expr(input, state, lib, settings.level_up())?), + }; + + let hash = if let Some(expr) = expr { + if let Some(value) = expr.get_constant_value() { + let hasher = &mut get_hasher(); + value.hash(hasher); + let hash = hasher.finish(); + + if table.contains_key(&hash) { + return Err(PERR::DuplicatedSwitchCase.into_err(expr.position())); + } + + Some(hash) + } else { + return Err(PERR::ExprExpected("a literal".to_string()).into_err(expr.position())); + } + } else { + None + }; + + match input.next().unwrap() { + (Token::DoubleArrow, _) => (), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (_, pos) => { + return Err(PERR::MissingToken( + Token::DoubleArrow.into(), + "in this switch case".to_string(), + ) + .into_err(pos)) + } + }; + + let stmt = parse_stmt(input, state, lib, settings.level_up()).map(Option::unwrap)?; + + let need_comma = !stmt.is_self_terminated(); + + def_stmt = if let Some(hash) = hash { + table.insert(hash, stmt); + None + } else { + Some(stmt) + }; + + match input.peek().unwrap() { + (Token::Comma, _) => { + eat_token(input, Token::Comma); + } + (Token::RightBrace, _) => (), + (Token::EOF, pos) => { + return Err( + PERR::MissingToken(Token::RightParen.into(), MISSING_RBRACE.into()) + .into_err(*pos), + ) + } + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), + (_, pos) if need_comma => { + return Err(PERR::MissingToken( + Token::Comma.into(), + "to separate the items in this switch block".into(), + ) + .into_err(*pos)) + } + (_, _) => (), + } + } + + Ok(Expr::Switch( + Box::new((item, table, def_stmt)), + settings.pos, + )) +} + /// Parse a primary expression. fn parse_primary( input: &mut TokenStream, @@ -782,9 +937,11 @@ fn parse_primary( let mut root_expr = match token { Token::IntegerConstant(x) => Expr::IntegerConstant(x, settings.pos), #[cfg(not(feature = "no_float"))] - Token::FloatConstant(x) => Expr::FloatConstant(FloatWrapper(x), settings.pos), + Token::FloatConstant(x) => Expr::FloatConstant(x, settings.pos), Token::CharConstant(c) => Expr::CharConstant(c, settings.pos), - Token::StringConstant(s) => Expr::StringConstant(Box::new(IdentX::new(s, settings.pos))), + Token::StringConstant(s) => { + Expr::StringConstant(state.get_interned_string(s), settings.pos) + } // Function call Token::Identifier(s) if *next_token == Token::LeftParen || *next_token == Token::Bang => { @@ -793,7 +950,8 @@ fn parse_primary( { state.allow_capture = true; } - Expr::Variable(Box::new((None, None, 0, Ident::new(s, settings.pos)))) + let var_name_def = IdentX::new(state.get_interned_string(s), settings.pos); + Expr::Variable(Box::new((None, None, 0, var_name_def))) } // Module qualification #[cfg(not(feature = "no_module"))] @@ -803,18 +961,21 @@ fn parse_primary( { state.allow_capture = true; } - Expr::Variable(Box::new((None, None, 0, Ident::new(s, settings.pos)))) + let var_name_def = IdentX::new(state.get_interned_string(s), settings.pos); + Expr::Variable(Box::new((None, None, 0, var_name_def))) } // Normal variable access Token::Identifier(s) => { let index = state.access_var(&s, settings.pos); - Expr::Variable(Box::new((index, None, 0, Ident::new(s, settings.pos)))) + let var_name_def = IdentX::new(state.get_interned_string(s), settings.pos); + Expr::Variable(Box::new((index, None, 0, var_name_def))) } // Function call is allowed to have reserved keyword Token::Reserved(s) if *next_token == Token::LeftParen || *next_token == Token::Bang => { if is_keyword_function(&s) { - Expr::Variable(Box::new((None, None, 0, Ident::new(s, settings.pos)))) + let var_name_def = IdentX::new(state.get_interned_string(s), settings.pos); + Expr::Variable(Box::new((None, None, 0, var_name_def))) } else { return Err(PERR::Reserved(s).into_err(settings.pos)); } @@ -829,7 +990,8 @@ fn parse_primary( ))) .into_err(settings.pos)); } else { - Expr::Variable(Box::new((None, None, 0, Ident::new(s, settings.pos)))) + let var_name_def = IdentX::new(state.get_interned_string(s), settings.pos); + Expr::Variable(Box::new((None, None, 0, var_name_def))) } } @@ -842,6 +1004,7 @@ fn parse_primary( Token::LeftBracket => parse_array_literal(input, state, lib, settings.level_up())?, #[cfg(not(feature = "no_object"))] Token::MapStart => parse_map_literal(input, state, lib, settings.level_up())?, + Token::Switch => parse_switch(input, state, lib, settings.level_up())?, Token::True => Expr::True(settings.pos), Token::False => Expr::False(settings.pos), Token::LexError(err) => return Err(err.into_err(settings.pos)), @@ -888,13 +1051,13 @@ fn parse_primary( .into_err(pos)); } - let (_, modules, _, Ident { name, pos }) = *x; + let (_, modules, _, IdentX { name, pos }) = *x; settings.pos = pos; parse_fn_call(input, state, lib, name, true, modules, settings.level_up())? } // Function call (Expr::Variable(x), Token::LeftParen) => { - let (_, modules, _, Ident { name, pos }) = *x; + let (_, modules, _, IdentX { name, pos }) = *x; settings.pos = pos; parse_fn_call(input, state, lib, name, false, modules, settings.level_up())? } @@ -907,12 +1070,13 @@ fn parse_primary( if let Some(ref mut modules) = modules { modules.push(var_name_def); } else { - let mut m: ModuleRef = Default::default(); + let mut m: NamespaceRef = Default::default(); m.push(var_name_def); modules = Some(Box::new(m)); } - Expr::Variable(Box::new((index, modules, 0, Ident::new(id2, pos2)))) + let var_name_def = IdentX::new(state.get_interned_string(id2), pos2); + Expr::Variable(Box::new((index, modules, 0, var_name_def))) } (Token::Reserved(id2), pos2) if is_valid_identifier(id2.chars()) => { return Err(PERR::Reserved(id2).into_err(pos2)); @@ -934,16 +1098,16 @@ fn parse_primary( } match &mut root_expr { - // Cache the hash key for module-qualified variables + // Cache the hash key for namespace-qualified variables Expr::Variable(x) if x.1.is_some() => { - let (_, modules, hash, Ident { name, .. }) = x.as_mut(); - let modules = modules.as_mut().unwrap(); + let (_, modules, hash, IdentX { name, .. }) = x.as_mut(); + let namespace = modules.as_mut().unwrap(); // Qualifiers + variable name - *hash = calc_script_fn_hash(modules.iter().map(|v| v.name.as_str()), name, 0); + *hash = calc_script_fn_hash(namespace.iter().map(|v| v.name.as_str()), name, 0); #[cfg(not(feature = "no_module"))] - modules.set_index(state.find_module(&modules[0].name)); + namespace.set_index(state.find_module(&namespace[0].name)); } _ => (), } @@ -983,7 +1147,7 @@ fn parse_unary( .map(|i| Expr::IntegerConstant(i, pos)) .or_else(|| { #[cfg(not(feature = "no_float"))] - return Some(Expr::FloatConstant(-Into::::into(num), pos)); + return Some(Expr::FloatConstant(-(num as FLOAT), pos)); #[cfg(feature = "no_float")] return None; }) @@ -1001,7 +1165,7 @@ fn parse_unary( args.push(expr); Ok(Expr::FnCall( - Box::new(FnCallInfo { + Box::new(FnCallExpr { name: op.into(), native_only: true, namespace: None, @@ -1030,7 +1194,7 @@ fn parse_unary( let hash = calc_script_fn_hash(empty(), op, 1); Ok(Expr::FnCall( - Box::new(FnCallInfo { + Box::new(FnCallExpr { name: op.into(), native_only: true, hash, @@ -1046,6 +1210,7 @@ fn parse_unary( Token::Pipe | Token::Or if settings.allow_anonymous_fn => { let mut new_state = ParseState::new( state.engine, + state.script_hash, #[cfg(not(feature = "unchecked"))] state.max_function_expr_depth, #[cfg(not(feature = "unchecked"))] @@ -1102,7 +1267,7 @@ fn make_assignment_stmt<'a>( index, _, _, - Ident { + IdentX { name, pos: name_pos, }, @@ -1113,7 +1278,7 @@ fn make_assignment_stmt<'a>( } // Constant values cannot be assigned to ScopeEntryType::Constant => { - Err(PERR::AssignmentToConstant(name.clone()).into_err(*name_pos)) + Err(PERR::AssignmentToConstant(name.to_string()).into_err(*name_pos)) } } } @@ -1129,7 +1294,7 @@ fn make_assignment_stmt<'a>( index, _, _, - Ident { + IdentX { name, pos: name_pos, }, @@ -1140,7 +1305,7 @@ fn make_assignment_stmt<'a>( } // Constant values cannot be assigned to ScopeEntryType::Constant => { - Err(PERR::AssignmentToConstant(name.clone()).into_err(*name_pos)) + Err(PERR::AssignmentToConstant(name.to_string()).into_err(*name_pos)) } } } @@ -1200,20 +1365,25 @@ fn parse_op_assignment_stmt( /// Make a dot expression. #[cfg(not(feature = "no_object"))] -fn make_dot_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { +fn make_dot_expr( + state: &mut ParseState, + lhs: Expr, + rhs: Expr, + op_pos: Position, +) -> Result { Ok(match (lhs, rhs) { // idx_lhs[idx_expr].rhs // Attach dot chain to the bottom level of indexing chain (Expr::Index(mut x, pos), rhs) => { - x.rhs = make_dot_expr(x.rhs, rhs, op_pos)?; + x.rhs = make_dot_expr(state, x.rhs, rhs, op_pos)?; Expr::Index(x, pos) } // lhs.id (lhs, Expr::Variable(x)) if x.1.is_none() => { let ident = x.3; - let getter = make_getter(&ident.name); - let setter = make_setter(&ident.name); - let rhs = Expr::Property(Box::new(((getter, setter), ident.into()))); + let getter = state.get_interned_string(make_getter(&ident.name)); + let setter = state.get_interned_string(make_setter(&ident.name)); + let rhs = Expr::Property(Box::new(((getter, setter), ident))); Expr::Dot(Box::new(BinaryExpr { lhs, rhs }), op_pos) } @@ -1229,7 +1399,7 @@ fn make_dot_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { let rhs = Expr::Dot( Box::new(BinaryExpr { - lhs: x.lhs.into_property(), + lhs: x.lhs.into_property(state), rhs: x.rhs, }), pos, @@ -1240,7 +1410,7 @@ fn make_dot_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { let rhs = Expr::Index( Box::new(BinaryExpr { - lhs: x.lhs.into_property(), + lhs: x.lhs.into_property(state), rhs: x.rhs, }), pos, @@ -1298,19 +1468,19 @@ fn make_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result (), + (Expr::StringConstant(_, _), Expr::StringConstant(_, _)) + | (Expr::CharConstant(_, _), Expr::StringConstant(_, _)) => (), // 123.456 in "xxxx" #[cfg(not(feature = "no_float"))] - (x @ Expr::FloatConstant(_, _), Expr::StringConstant(_)) => { + (x @ Expr::FloatConstant(_, _), Expr::StringConstant(_, _)) => { return Err(PERR::MalformedInExpr( "'in' expression for a string expects a string, not a float".into(), ) .into_err(x.position())) } // 123 in "xxxx" - (x @ Expr::IntegerConstant(_, _), Expr::StringConstant(_)) => { + (x @ Expr::IntegerConstant(_, _), Expr::StringConstant(_, _)) => { return Err(PERR::MalformedInExpr( "'in' expression for a string expects a string, not a number".into(), ) @@ -1318,32 +1488,32 @@ fn make_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { + (x @ Expr::And(_, _), Expr::StringConstant(_, _)) + | (x @ Expr::Or(_, _), Expr::StringConstant(_, _)) + | (x @ Expr::In(_, _), Expr::StringConstant(_, _)) + | (x @ Expr::True(_), Expr::StringConstant(_, _)) + | (x @ Expr::False(_), Expr::StringConstant(_, _)) => { return Err(PERR::MalformedInExpr( "'in' expression for a string expects a string, not a boolean".into(), ) .into_err(x.position())) } // [???, ???, ???] in "xxxx" - (x @ Expr::Array(_, _), Expr::StringConstant(_)) => { + (x @ Expr::Array(_, _), Expr::StringConstant(_, _)) => { return Err(PERR::MalformedInExpr( "'in' expression for a string expects a string, not an array".into(), ) .into_err(x.position())) } // #{...} in "xxxx" - (x @ Expr::Map(_, _), Expr::StringConstant(_)) => { + (x @ Expr::Map(_, _), Expr::StringConstant(_, _)) => { return Err(PERR::MalformedInExpr( "'in' expression for a string expects a string, not an object map".into(), ) .into_err(x.position())) } // () in "xxxx" - (x @ Expr::Unit(_), Expr::StringConstant(_)) => { + (x @ Expr::Unit(_), Expr::StringConstant(_, _)) => { return Err(PERR::MalformedInExpr( "'in' expression for a string expects a string, not ()".into(), ) @@ -1351,7 +1521,7 @@ fn make_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result (), // 123.456 in #{...} @@ -1490,7 +1660,7 @@ fn parse_binary_op( let op = op_token.syntax(); let hash = calc_script_fn_hash(empty(), &op, 2); - let op_base = FnCallInfo { + let op_base = FnCallExpr { name: op, native_only: true, capture: false, @@ -1513,7 +1683,7 @@ fn parse_binary_op( | Token::Ampersand | Token::Pipe | Token::XOr => Expr::FnCall( - Box::new(FnCallInfo { + Box::new(FnCallExpr { hash, args, ..op_base @@ -1523,7 +1693,7 @@ fn parse_binary_op( // '!=' defaults to true when passed invalid operands Token::NotEqualsTo => Expr::FnCall( - Box::new(FnCallInfo { + Box::new(FnCallExpr { hash, args, def_value: Some(true), @@ -1538,7 +1708,7 @@ fn parse_binary_op( | Token::LessThanEqualsTo | Token::GreaterThan | Token::GreaterThanEqualsTo => Expr::FnCall( - Box::new(FnCallInfo { + Box::new(FnCallExpr { hash, args, def_value: cmp_def, @@ -1579,13 +1749,13 @@ fn parse_binary_op( Token::Period => { let rhs = args.pop().unwrap(); let current_lhs = args.pop().unwrap(); - make_dot_expr(current_lhs, rhs, pos)? + make_dot_expr(state, current_lhs, rhs, pos)? } Token::Custom(s) if state.engine.custom_keywords.contains_key(&s) => { // Accept non-native functions for custom operators Expr::FnCall( - Box::new(FnCallInfo { + Box::new(FnCallExpr { hash, args, native_only: false, @@ -1617,7 +1787,7 @@ fn parse_custom_syntax( delta if delta > 0 => { state.stack.resize( state.stack.len() + delta as usize, - ("".to_string(), ScopeEntryType::Normal), + ("".into(), ScopeEntryType::Normal), ); } delta if delta < 0 && state.stack.len() <= delta.abs() as usize => state.stack.clear(), @@ -1647,12 +1817,8 @@ fn parse_custom_syntax( MARKER_IDENT => match input.next().unwrap() { (Token::Identifier(s), pos) => { segments.push(s.clone()); - exprs.push(Expr::Variable(Box::new(( - None, - None, - 0, - Ident::new(s, pos), - )))); + let var_name_def = IdentX::new(state.get_interned_string(s), pos); + exprs.push(Expr::Variable(Box::new((None, None, 0, var_name_def)))); } (Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => { return Err(PERR::Reserved(s).into_err(pos)); @@ -1901,8 +2067,9 @@ fn parse_for( ensure_not_statement_expr(input, "a boolean")?; let expr = parse_expr(input, state, lib, settings.level_up())?; + let loop_var = state.get_interned_string(name.clone()); let prev_stack_len = state.stack.len(); - state.stack.push((name.clone(), ScopeEntryType::Normal)); + state.stack.push((loop_var, ScopeEntryType::Normal)); settings.is_breakable = true; let body = parse_block(input, state, lib, settings.level_up())?; @@ -1949,15 +2116,17 @@ fn parse_let( match var_type { // let name = expr ScopeEntryType::Normal => { - state.stack.push((name.clone(), ScopeEntryType::Normal)); - let ident = Ident::new(name, pos); - Ok(Stmt::Let(Box::new(ident), init_expr, export, token_pos)) + let var_name = state.get_interned_string(name.clone()); + state.stack.push((var_name, ScopeEntryType::Normal)); + let var_def = Ident::new(name, pos); + Ok(Stmt::Let(Box::new(var_def), init_expr, export, token_pos)) } // const name = { expr:constant } ScopeEntryType::Constant => { - state.stack.push((name.clone(), ScopeEntryType::Constant)); - let ident = Ident::new(name, pos); - Ok(Stmt::Const(Box::new(ident), init_expr, export, token_pos)) + let var_name = state.get_interned_string(name.clone()); + state.stack.push((var_name, ScopeEntryType::Constant)); + let var_def = Ident::new(name, pos); + Ok(Stmt::Const(Box::new(var_def), init_expr, export, token_pos)) } } } @@ -1995,6 +2164,7 @@ fn parse_import( (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; + let name = state.get_interned_string(name); state.modules.push(name.clone()); Ok(Stmt::Import( @@ -2048,7 +2218,7 @@ fn parse_export( let rename = if match_token(input, Token::As).0 { match input.next().unwrap() { - (Token::Identifier(s), pos) => Some(Ident::new(s.clone(), pos)), + (Token::Identifier(s), pos) => Some(IdentX::new(state.get_interned_string(s), pos)), (Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => { return Err(PERR::Reserved(s).into_err(pos)); } @@ -2059,7 +2229,7 @@ fn parse_export( None }; - exports.push((Ident::new(id, id_pos), rename)); + exports.push((IdentX::new(state.get_interned_string(id), id_pos), rename)); match input.peek().unwrap() { (Token::Comma, _) => { @@ -2112,10 +2282,7 @@ fn parse_block( // Parse statements inside the block settings.is_global = false; - let stmt = match parse_stmt(input, state, lib, settings.level_up())? { - Some(s) => s, - None => continue, - }; + let stmt = parse_stmt(input, state, lib, settings.level_up()).map(Option::unwrap)?; // See if it needs a terminating semicolon let need_semicolon = !stmt.is_self_terminated(); @@ -2216,6 +2383,7 @@ fn parse_stmt( (Token::Fn, pos) => { let mut new_state = ParseState::new( state.engine, + state.script_hash, #[cfg(not(feature = "unchecked"))] state.max_function_expr_depth, #[cfg(not(feature = "unchecked"))] @@ -2421,6 +2589,10 @@ fn parse_fn( match input.next().unwrap() { (Token::RightParen, _) => break, (Token::Identifier(s), pos) => { + if params.iter().any(|(p, _)| p == &s) { + return Err(PERR::FnDuplicatedParam(name.to_string(), s).into_err(pos)); + } + let s = state.get_interned_string(s); state.stack.push((s.clone(), ScopeEntryType::Normal)); params.push((s, pos)) } @@ -2445,21 +2617,6 @@ fn parse_fn( } } - // Check for duplicating parameters - params - .iter() - .enumerate() - .try_for_each(|(i, (p1, _))| { - params - .iter() - .skip(i + 1) - .find(|(p2, _)| p2 == p1) - .map_or_else(|| Ok(()), |(p2, pos)| Err((p2, *pos))) - }) - .map_err(|(p, pos)| { - PERR::FnDuplicatedParam(name.to_string(), p.to_string()).into_err(pos) - })?; - // Parse function body let body = match input.peek().unwrap() { (Token::LeftBrace, _) => { @@ -2495,7 +2652,7 @@ fn parse_fn( /// Creates a curried expression from a list of external variables #[cfg(not(feature = "no_function"))] -fn make_curry_from_externals(fn_expr: Expr, externals: StaticVec, pos: Position) -> Expr { +fn make_curry_from_externals(fn_expr: Expr, externals: StaticVec, pos: Position) -> Expr { if externals.is_empty() { return fn_expr; } @@ -2507,18 +2664,18 @@ fn make_curry_from_externals(fn_expr: Expr, externals: StaticVec, pos: Po #[cfg(not(feature = "no_closure"))] externals.iter().for_each(|x| { - args.push(Expr::Variable(Box::new((None, None, 0, x.clone())))); + args.push(Expr::Variable(Box::new((None, None, 0, x.clone().into())))); }); #[cfg(feature = "no_closure")] externals.into_iter().for_each(|x| { - args.push(Expr::Variable(Box::new((None, None, 0, x.clone())))); + args.push(Expr::Variable(Box::new((None, None, 0, x.clone().into())))); }); let hash = calc_script_fn_hash(empty(), KEYWORD_FN_PTR_CURRY, num_externals + 1); let expr = Expr::FnCall( - Box::new(FnCallInfo { + Box::new(FnCallExpr { name: KEYWORD_FN_PTR_CURRY.into(), hash, args, @@ -2534,7 +2691,7 @@ fn make_curry_from_externals(fn_expr: Expr, externals: StaticVec, pos: Po // Statement block let mut statements: StaticVec<_> = Default::default(); // Insert `Share` statements - statements.extend(externals.into_iter().map(|x| Stmt::Share(Box::new(x)))); + statements.extend(externals.into_iter().map(|x| Stmt::Share(x))); // Final expression statements.push(Stmt::Expr(expr)); Expr::Stmt(Box::new(statements), pos) @@ -2563,6 +2720,10 @@ fn parse_anon_fn( match input.next().unwrap() { (Token::Pipe, _) => break, (Token::Identifier(s), pos) => { + if params.iter().any(|(p, _)| p == &s) { + return Err(PERR::FnDuplicatedParam("".to_string(), s).into_err(pos)); + } + let s = state.get_interned_string(s); state.stack.push((s.clone(), ScopeEntryType::Normal)); params.push((s, pos)) } @@ -2592,34 +2753,19 @@ fn parse_anon_fn( } } - // Check for duplicating parameters - params - .iter() - .enumerate() - .try_for_each(|(i, (p1, _))| { - params - .iter() - .skip(i + 1) - .find(|(p2, _)| p2 == p1) - .map_or_else(|| Ok(()), |(p2, pos)| Err((p2, *pos))) - }) - .map_err(|(p, pos)| PERR::FnDuplicatedParam("".to_string(), p.to_string()).into_err(pos))?; - // Parse function body settings.is_breakable = false; - let pos = input.peek().unwrap().1; - let body = parse_stmt(input, state, lib, settings.level_up()) - .map(|stmt| stmt.unwrap_or_else(|| Stmt::Noop(pos)))?; + let body = parse_stmt(input, state, lib, settings.level_up()).map(Option::unwrap)?; // External variables may need to be processed in a consistent order, // so extract them into a list. - let externals: StaticVec = { + let externals: StaticVec = { #[cfg(not(feature = "no_closure"))] { state .externals .iter() - .map(|(k, &v)| Ident::new(k.clone(), v)) + .map(|(k, &v)| IdentX::new(k.clone(), v)) .collect() } #[cfg(feature = "no_closure")] @@ -2636,18 +2782,12 @@ fn parse_anon_fn( params.into_iter().map(|(v, _)| v).collect() }; - // Calculate hash - #[cfg(feature = "no_std")] - let mut s: AHasher = Default::default(); - #[cfg(not(feature = "no_std"))] - let mut s = DefaultHasher::new(); + // Create unique function name by hashing the script hash plus the position + let hasher = &mut get_hasher(); + state.script_hash.hash(hasher); + settings.pos.hash(hasher); + let hash = hasher.finish(); - s.write_usize(params.len()); - params.iter().for_each(|a| a.hash(&mut s)); - body.hash(&mut s); - let hash = s.finish(); - - // Create unique function name let fn_name: ImmutableString = format!("{}{:016x}", FN_ANONYMOUS, hash).into(); // Define the function @@ -2663,7 +2803,7 @@ fn parse_anon_fn( mods: Default::default(), }; - let expr = Expr::FnPointer(Box::new(IdentX::new(fn_name, settings.pos))); + let expr = Expr::FnPointer(fn_name, settings.pos); let expr = if cfg!(not(feature = "no_closure")) { make_curry_from_externals(expr, externals, settings.pos) @@ -2677,6 +2817,7 @@ fn parse_anon_fn( impl Engine { pub(crate) fn parse_global_expr( &self, + script_hash: u64, input: &mut TokenStream, scope: &Scope, optimization_level: OptimizationLevel, @@ -2684,6 +2825,7 @@ impl Engine { let mut functions = Default::default(); let mut state = ParseState::new( self, + script_hash, #[cfg(not(feature = "unchecked"))] self.max_expr_depth(), #[cfg(not(feature = "unchecked"))] @@ -2727,12 +2869,14 @@ impl Engine { /// Parse the global level statements. fn parse_global_level( &self, + script_hash: u64, input: &mut TokenStream, ) -> Result<(Vec, Vec), ParseError> { let mut statements: Vec = Default::default(); let mut functions = Default::default(); let mut state = ParseState::new( self, + script_hash, #[cfg(not(feature = "unchecked"))] self.max_expr_depth(), #[cfg(not(feature = "unchecked"))] @@ -2793,11 +2937,12 @@ impl Engine { #[inline(always)] pub(crate) fn parse( &self, + script_hash: u64, input: &mut TokenStream, scope: &Scope, optimization_level: OptimizationLevel, ) -> Result { - let (statements, lib) = self.parse_global_level(input)?; + let (statements, lib) = self.parse_global_level(script_hash, input)?; Ok( // Optimize AST @@ -2812,12 +2957,12 @@ impl Engine { pub fn map_dynamic_to_expr(value: Dynamic, pos: Position) -> Option { match value.0 { #[cfg(not(feature = "no_float"))] - Union::Float(value) => Some(Expr::FloatConstant(FloatWrapper(value), pos)), + Union::Float(value) => Some(Expr::FloatConstant(value, pos)), Union::Unit(_) => Some(Expr::Unit(pos)), Union::Int(value) => Some(Expr::IntegerConstant(value, pos)), Union::Char(value) => Some(Expr::CharConstant(value, pos)), - Union::Str(value) => Some(Expr::StringConstant(Box::new(IdentX::new(value, pos)))), + Union::Str(value) => Some(Expr::StringConstant(value, pos)), Union::Bool(true) => Some(Expr::True(pos)), Union::Bool(false) => Some(Expr::False(pos)), #[cfg(not(feature = "no_index"))] diff --git a/src/syntax.rs b/src/syntax.rs index 4baa5a7a..7a60d787 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -34,7 +34,7 @@ pub type FnCustomSyntaxParse = dyn Fn(&[String]) -> Result, ParseError> + Send + Sync; /// An expression sub-tree in an AST. -#[derive(Debug, Clone, Hash)] +#[derive(Debug, Clone)] pub struct Expression<'a>(&'a Expr); impl<'a> From<&'a Expr> for Expression<'a> { diff --git a/src/token.rs b/src/token.rs index a27dcd82..3a7ef783 100644 --- a/src/token.rs +++ b/src/token.rs @@ -216,6 +216,10 @@ pub enum Token { Colon, /// `::` DoubleColon, + /// `=>` + DoubleArrow, + /// `_` + Underscore, /// `,` Comma, /// `.` @@ -236,6 +240,8 @@ pub enum Token { If, /// `else` Else, + /// `switch` + Switch, /// `while` While, /// `loop` @@ -371,6 +377,8 @@ impl Token { SemiColon => ";", Colon => ":", DoubleColon => "::", + DoubleArrow => "=>", + Underscore => "_", Comma => ",", Period => ".", MapStart => "#{", @@ -381,6 +389,7 @@ impl Token { Const => "const", If => "if", Else => "else", + Switch => "switch", While => "while", Loop => "loop", For => "for", @@ -455,6 +464,8 @@ impl Token { ";" => SemiColon, ":" => Colon, "::" => DoubleColon, + "=>" => DoubleArrow, + "_" => Underscore, "," => Comma, "." => Period, "#{" => MapStart, @@ -465,6 +476,7 @@ impl Token { "const" => Const, "if" => If, "else" => Else, + "switch" => Switch, "while" => While, "loop" => Loop, "for" => For, @@ -521,11 +533,12 @@ impl Token { #[cfg(feature = "no_module")] "import" | "export" | "as" => Reserved(syntax.into()), - "===" | "!==" | "->" | "<-" | "=>" | ":=" | "::<" | "(*" | "*)" | "#" | "public" - | "new" | "use" | "module" | "package" | "var" | "static" | "shared" | "with" - | "do" | "each" | "then" | "goto" | "exit" | "switch" | "match" | "case" - | "default" | "void" | "null" | "nil" | "spawn" | "thread" | "go" | "sync" - | "async" | "await" | "yield" => Reserved(syntax.into()), + "===" | "!==" | "->" | "<-" | ":=" | "::<" | "(*" | "*)" | "#" | "public" | "new" + | "use" | "module" | "package" | "var" | "static" | "shared" | "with" | "do" + | "each" | "then" | "goto" | "exit" | "match" | "case" | "default" | "void" + | "null" | "nil" | "spawn" | "thread" | "go" | "sync" | "async" | "await" | "yield" => { + Reserved(syntax.into()) + } KEYWORD_PRINT | KEYWORD_DEBUG | KEYWORD_TYPE_OF | KEYWORD_EVAL | KEYWORD_FN_PTR | KEYWORD_FN_PTR_CALL | KEYWORD_FN_PTR_CURRY | KEYWORD_IS_DEF_VAR @@ -1318,7 +1331,7 @@ fn get_next_token_inner( } ('=', '>') => { eat_next(stream, pos); - return Some((Token::Reserved("=>".into()), start_pos)); + return Some((Token::DoubleArrow, start_pos)); } ('=', _) => return Some((Token::Equals, start_pos)), @@ -1481,7 +1494,11 @@ fn get_identifier( let is_valid_identifier = is_valid_identifier(result.iter().cloned()); - let identifier = result.into_iter().collect(); + let identifier: String = result.into_iter().collect(); + + if let Some(token) = Token::lookup_from_syntax(&identifier) { + return Some((token, start_pos)); + } if !is_valid_identifier { return Some(( @@ -1490,10 +1507,7 @@ fn get_identifier( )); } - return Some(( - Token::lookup_from_syntax(&identifier).unwrap_or_else(|| Token::Identifier(identifier)), - start_pos, - )); + return Some((Token::Identifier(identifier), start_pos)); } /// Is this keyword allowed as a function? @@ -1654,9 +1668,6 @@ impl<'a> Iterator for TokenIterator<'a, '_> { ("<-", false) => Token::LexError(LERR::ImproperSymbol( "'<-' is not a valid symbol. This is not Go! Should it be '<='?".to_string(), )), - ("=>", false) => Token::LexError(LERR::ImproperSymbol( - "'=>' is not a valid symbol. This is not Rust! Should it be '>='?".to_string(), - )), (":=", false) => Token::LexError(LERR::ImproperSymbol( "':=' is not a valid assignment operator. This is not Go! Should it be simply '='?".to_string(), )), @@ -1735,7 +1746,7 @@ impl Engine { engine: self, state: TokenizeState { #[cfg(not(feature = "unchecked"))] - max_string_size: self.limits_set.max_string_size, + max_string_size: self.limits.max_string_size, #[cfg(feature = "unchecked")] max_string_size: 0, non_unary: false, diff --git a/src/utils.rs b/src/utils.rs index 385c4536..672fb4ec 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -55,7 +55,7 @@ impl BuildHasher for StraightHasherBuilder { } } -/// _[INTERNALS]_ Calculate a `u64` hash key from a module-qualified function name and parameter types. +/// _[INTERNALS]_ Calculate a `u64` hash key from a namespace-qualified function name and parameter types. /// Exported under the `internals` feature only. /// /// Module names are passed in via `&str` references from an iterator. @@ -73,8 +73,8 @@ pub fn calc_native_fn_hash<'a>( calc_fn_hash(modules, fn_name, None, params) } -/// _[INTERNALS]_ Calculate a `u64` hash key from a module-qualified function name and the number of parameters, -/// but no parameter types. +/// _[INTERNALS]_ Calculate a `u64` hash key from a namespace-qualified function name +/// and the number of parameters, but no parameter types. /// Exported under the `internals` feature only. /// /// Module names are passed in via `&str` references from an iterator. @@ -92,7 +92,17 @@ pub fn calc_script_fn_hash<'a>( calc_fn_hash(modules, fn_name, Some(num), empty()) } -/// Calculate a `u64` hash key from a module-qualified function name and parameter types. +/// Create an instance of the default hasher. +pub fn get_hasher() -> impl Hasher { + #[cfg(feature = "no_std")] + let s: AHasher = Default::default(); + #[cfg(not(feature = "no_std"))] + let s = DefaultHasher::new(); + + s +} + +/// Calculate a `u64` hash key from a namespace-qualified function name and parameter types. /// /// Module names are passed in via `&str` references from an iterator. /// Parameter types are passed in via `TypeId` values from an iterator. @@ -106,10 +116,7 @@ fn calc_fn_hash<'a>( num: Option, params: impl Iterator, ) -> u64 { - #[cfg(feature = "no_std")] - let s: &mut AHasher = &mut Default::default(); - #[cfg(not(feature = "no_std"))] - let s = &mut DefaultHasher::new(); + let s = &mut get_hasher(); // We always skip the first module modules.skip(1).for_each(|m| m.hash(s)); diff --git a/tests/bool_op.rs b/tests/bool_op.rs index 84c3d44f..0520736d 100644 --- a/tests/bool_op.rs +++ b/tests/bool_op.rs @@ -40,9 +40,8 @@ fn test_bool_op_short_circuit() -> Result<(), Box> { engine.eval::( r" let x = true; - x || { throw; }; - " + " )?, true ); @@ -51,9 +50,8 @@ fn test_bool_op_short_circuit() -> Result<(), Box> { engine.eval::( r" let x = false; - x && { throw; }; - " + " )?, false ); @@ -68,10 +66,9 @@ fn test_bool_op_no_short_circuit1() { assert!(engine .eval::( r" - let x = true; - - x | { throw; } - " + let x = true; + x | { throw; } + " ) .is_err()); } @@ -83,10 +80,9 @@ fn test_bool_op_no_short_circuit2() { assert!(engine .eval::( r" - let x = false; - - x & { throw; } - " + let x = false; + x & { throw; } + " ) .is_err()); } diff --git a/tests/fn_ptr.rs b/tests/fn_ptr.rs index c6658f17..6a37af62 100644 --- a/tests/fn_ptr.rs +++ b/tests/fn_ptr.rs @@ -73,7 +73,8 @@ fn test_fn_ptr() -> Result<(), Box> { "# ) .expect_err("should error"), - EvalAltResult::ErrorInFunctionCall(fn_name, err, _) if fn_name == "foo" && matches!(*err, EvalAltResult::ErrorUnboundThis(_)) + EvalAltResult::ErrorInFunctionCall(fn_name, err, _) + if fn_name == "foo" && matches!(*err, EvalAltResult::ErrorUnboundThis(_)) )); Ok(()) diff --git a/tests/switch.rs b/tests/switch.rs new file mode 100644 index 00000000..64fe9d31 --- /dev/null +++ b/tests/switch.rs @@ -0,0 +1,64 @@ +use rhai::{Engine, EvalAltResult, Scope, INT}; + +#[test] +fn test_switch() -> Result<(), Box> { + let engine = Engine::new(); + let mut scope = Scope::new(); + scope.push("x", 42 as INT); + + assert_eq!( + engine.eval_with_scope::(&mut scope, "switch x { 1 => (), 2 => 'a', 42 => true }")?, + true + ); + assert_eq!( + engine.eval_with_scope::(&mut scope, "switch x { 1 => (), 2 => 'a', _ => true }")?, + true + ); + assert_eq!( + engine.eval_with_scope::<()>(&mut scope, "switch x { 1 => 123, 2 => 'a' }")?, + () + ); + assert_eq!( + engine.eval_with_scope::( + &mut scope, + "switch x { 1 => 123, 42 => { x / 2 }, _ => 999 }" + )?, + 21 + ); + #[cfg(not(feature = "no_index"))] + assert_eq!( + engine.eval_with_scope::( + &mut scope, + r" + let y = [1, 2, 3]; + + switch y { + 42 => 1, + true => 2, + [1, 2, 3] => 3, + _ => 9 + } + " + )?, + 3 + ); + #[cfg(not(feature = "no_object"))] + assert_eq!( + engine.eval_with_scope::( + &mut scope, + r" + let y = #{a:1, b:true, c:'x'}; + + switch y { + 42 => 1, + true => 2, + #{b:true, c:'x', a:1} => 3, + _ => 9 + } + " + )?, + 3 + ); + + Ok(()) +}