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