From 55b4907f19d5abded62db62afb0877899e9bc392 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 13 Nov 2020 18:32:18 +0800 Subject: [PATCH] Add switch expression. --- RELEASES.md | 11 ++ doc/src/SUMMARY.md | 21 +-- doc/src/appendix/keywords.md | 2 +- doc/src/appendix/operators.md | 3 +- doc/src/engine/dsl.md | 4 +- doc/src/engine/scope.md | 2 +- doc/src/language/dynamic.md | 30 ++-- doc/src/language/switch.md | 97 +++++++++++++ src/ast.rs | 130 ++++------------- src/dynamic.rs | 15 +- src/engine.rs | 23 ++- src/engine_api.rs | 32 ++++- src/optimize.rs | 42 +++++- src/parse_error.rs | 4 + src/parser.rs | 263 +++++++++++++++++++++++----------- src/syntax.rs | 2 +- src/token.rs | 39 +++-- src/utils.rs | 15 +- tests/switch.rs | 64 +++++++++ 19 files changed, 547 insertions(+), 252 deletions(-) create mode 100644 doc/src/language/switch.md create mode 100644 tests/switch.rs diff --git a/RELEASES.md b/RELEASES.md index 48ef1e69..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 ============== 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/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/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/src/ast.rs b/src/ast.rs index 268b9126..b0a4ac00 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -5,7 +5,7 @@ use crate::fn_native::{FnPtr, Shared}; 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; @@ -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; @@ -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), @@ -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,51 +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 -/// for floating-point numbers, allowing `Expr` to derive `Hash`. -/// -/// ## 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) { - TypeId::of::().hash(state); - 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) - } -} - /// _[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, @@ -877,7 +830,7 @@ pub struct BinaryExpr { /// ## WARNING /// /// This type is volatile and may change. -#[derive(Debug, Clone, Hash, Default)] +#[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, @@ -905,19 +858,29 @@ pub struct FnCallExpr { /// ## 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(ImmutableString, Position), /// FnPtr constant. FnPointer(ImmutableString, Position), + /// [ expr, ... ] + Array(Box>, Position), + /// #{ name:expr, ... } + Map(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 @@ -932,22 +895,21 @@ pub enum Expr { Dot(Box, Position), /// expr[expr] Index(Box, Position), - /// [ expr, ... ] - Array(Box>, Position), - /// #{ name:expr, ... } - Map(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), - /// true - True(Position), - /// false - False(Position), - /// () - Unit(Position), /// Custom syntax Custom(Box, Position), } @@ -997,7 +959,7 @@ 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.clone().into(), Self::FnPointer(x, _) => Dynamic(Union::FnPtr(Box::new(FnPtr::new_unchecked( @@ -1052,6 +1014,7 @@ impl Expr { 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(), @@ -1083,6 +1046,7 @@ impl Expr { 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, @@ -1124,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 { @@ -1211,6 +1142,7 @@ impl Expr { Self::StringConstant(_, _) | Self::Stmt(_, _) | Self::FnCall(_, _) + | Self::Switch(_, _) | Self::Dot(_, _) | Self::Index(_, _) | Self::Array(_, _) diff --git a/src/dynamic.rs b/src/dynamic.rs index cd4002d2..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}; @@ -378,10 +378,15 @@ impl Hash for Dynamic { #[cfg(not(feature = "no_index"))] Union::Array(a) => a.hash(state), #[cfg(not(feature = "no_object"))] - Union::Map(m) => m.iter().for_each(|(key, item)| { - key.hash(state); - item.hash(state); - }), + 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"))] diff --git a/src/engine.rs b/src/engine.rs index 569c9bd2..16558bb5 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -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, @@ -1543,7 +1545,7 @@ impl Engine { Expr::IntegerConstant(x, _) => Ok((*x).into()), #[cfg(not(feature = "no_float"))] - Expr::FloatConstant(x, _) => Ok(x.0.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.clone(), Default::default()).into()), @@ -1666,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() diff --git a/src/engine_api.rs b/src/engine_api.rs index 3207fc94..1b50adf6 100644 --- a/src/engine_api.rs +++ b/src/engine_api.rs @@ -27,7 +27,7 @@ use crate::{ use crate::fn_register::{RegisterFn, RegisterResultFn}; #[cfg(not(feature = "no_function"))] -use crate::{fn_args::FuncArgs, fn_call::ensure_no_data_race, module::Module, StaticVec}; +use crate::{fn_args::FuncArgs, fn_call::ensure_no_data_race, module::Module, StaticVec, utils::get_hasher}; #[cfg(not(feature = "no_optimize"))] use crate::optimize::optimize_into_ast; @@ -35,6 +35,7 @@ use crate::optimize::optimize_into_ast; use crate::stdlib::{ any::{type_name, TypeId}, boxed::Box, + hash::{Hash, Hasher}, string::String, }; @@ -45,6 +46,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 +915,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 +1070,7 @@ impl Engine { .into()); }; + let hash = calc_hash_for_scripts(&scripts); let stream = self.lex( &scripts, if has_null { @@ -1073,8 +1083,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,10 +1169,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) + self.parse_global_expr(hash, &mut peekable, scope, self.optimization_level) } /// Evaluate a script file. @@ -1315,10 +1330,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) } @@ -1445,8 +1462,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/optimize.rs b/src/optimize.rs index af617d83..11e4fae3 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -11,6 +11,7 @@ use crate::module::Module; use crate::parser::map_dynamic_to_expr; use crate::scope::Scope; 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,6 +19,7 @@ use crate::ast::ReturnType; use crate::stdlib::{ boxed::Box, + hash::{Hash, Hasher}, iter::empty, mem, string::{String, ToString}, @@ -169,7 +171,7 @@ fn optimize_stmt_block( // 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_literal() => { + 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 @@ -695,6 +697,42 @@ fn optimize_expr(expr: &mut Expr, state: &mut State) { *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, _) => x.keywords.iter_mut().for_each(|expr| optimize_expr(expr, state)), @@ -746,7 +784,7 @@ fn optimize( let value_expr = expr.as_mut().unwrap(); optimize_expr(value_expr, &mut state); - if value_expr.is_literal() { + if value_expr.is_constant() { state.push_constant(&var_def.name, value_expr.clone()); } 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 0d694065..4d6091e3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -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::{ImmutableString, 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}; @@ -33,7 +33,7 @@ use crate::stdlib::{ boxed::Box, collections::HashMap, format, - hash::Hash, + hash::{Hash, Hasher}, iter::empty, num::NonZeroUsize, string::{String, ToString}, @@ -41,28 +41,19 @@ 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. @@ -93,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"))] @@ -100,6 +92,7 @@ impl<'e> ParseState<'e> { ) -> Self { Self { engine, + script_hash, #[cfg(not(feature = "unchecked"))] max_expr_depth, #[cfg(not(feature = "unchecked"))] @@ -633,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( @@ -648,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); @@ -660,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) => { @@ -691,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() { @@ -701,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)); } @@ -775,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, @@ -819,7 +937,7 @@ 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(state.get_interned_string(s), settings.pos) @@ -886,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)), @@ -1028,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; }) @@ -1091,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"))] @@ -2162,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(); @@ -2266,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"))] @@ -2471,6 +2589,9 @@ 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)) @@ -2496,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, _) => { @@ -2614,6 +2720,9 @@ 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)) @@ -2644,24 +2753,9 @@ 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. @@ -2688,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 @@ -2729,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, @@ -2736,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"))] @@ -2779,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"))] @@ -2845,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 @@ -2864,7 +2957,7 @@ 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)), 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 f109dc97..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(), )), diff --git a/src/utils.rs b/src/utils.rs index 6cfd4d2f..672fb4ec 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -92,6 +92,16 @@ pub fn calc_script_fn_hash<'a>( calc_fn_hash(modules, fn_name, Some(num), empty()) } +/// 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. @@ -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/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(()) +}