From c7820391b74aaf5c5633d61c8698e61786d627f1 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 8 Jul 2022 00:48:29 +0800 Subject: [PATCH 1/7] Remove examples from package. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9c22483f..89f9a18a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ homepage = "https://rhai.rs" repository = "https://github.com/rhaiscript" readme = "README.md" license = "MIT OR Apache-2.0" -include = ["**/*.rs", "scripts/*.rhai", "**/*.md", "Cargo.toml"] +include = ["/src/**/*.rs", "/Cargo.toml", "/README.md"] keywords = ["scripting", "scripting-engine", "scripting-language", "embedded"] categories = ["no-std", "embedded", "wasm", "parser-implementations"] From 08254c100f454cf37bc51dbd772321251b37d781 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 12 Jul 2022 16:19:55 +0800 Subject: [PATCH 2/7] Remove forum. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index b070ca1e..6e2082f9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ Rhai - Embedded Scripting for Rust [![VS Code plugin installs](https://img.shields.io/visual-studio-marketplace/i/rhaiscript.vscode-rhai?logo=visual-studio-code&label=vs%20code)](https://marketplace.visualstudio.com/items?itemName=rhaiscript.vscode-rhai) [![Sublime Text package downloads](https://img.shields.io/packagecontrol/dt/Rhai.svg?logo=sublime-text&label=sublime%20text)](https://packagecontrol.io/packages/Rhai) [![Discord Chat](https://img.shields.io/discord/767611025456889857.svg?logo=discord&label=discord)](https://discord.gg/HquqbYFcZ9) -[![Forum](https://img.shields.io/discourse/topics?server=https%3A%2F%2Frhai.discourse.group&logo=discourse&label=forum)](https://rhai.discourse.group/) [![Zulip Chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg?logo=zulip)](https://rhaiscript.zulipchat.com) [![Reddit Channel](https://img.shields.io/reddit/subreddit-subscribers/Rhai?logo=reddit&label=reddit)](https://www.reddit.com/r/Rhai) From 0555069de037aaa1bb89f6d28129a434ff518230 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 13 Jul 2022 19:21:13 +0800 Subject: [PATCH 3/7] Bump minimum Rust version. --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- README.md | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e14595d1..80d9917d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Rhai Release Notes Version 1.9.0 ============= +The minimum Rust version is now `1.60.0` in order to use the `dep:` syntax for dependencies. + New features ------------ diff --git a/Cargo.toml b/Cargo.toml index 56441c1b..684e2592 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = [".", "codegen"] [package] name = "rhai" version = "1.8.0" -rust-version = "1.60" +rust-version = "1.60.0" edition = "2018" authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung", "jhwgh1968"] description = "Embedded scripting for Rust" diff --git a/README.md b/README.md index 8262e8f0..48c5a27e 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Targets and builds * All CPU and O/S targets supported by Rust, including: * WebAssembly (WASM) * `no-std` -* Minimum Rust version 1.60 +* Minimum Rust version 1.60.0 Standard features From a12401a1fe464bf8bba6f21af983d0797ba5a721 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 17 Jul 2022 12:09:19 +0800 Subject: [PATCH 4/7] New range variant. --- CHANGELOG.md | 1 + src/packages/iter_basic.rs | 31 ++++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d9917d..73f3a40c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Enhancements * `switch` cases can now include multiple values separated by `|`. * `EvalContext::eval_expression_tree_raw` and `Expression::eval_with_context_raw` are added to allow for not rewinding the `Scope` at the end of a statements block. +* A new `range` function variant that takes an exclusive range with a step. Version 1.8.0 diff --git a/src/packages/iter_basic.rs b/src/packages/iter_basic.rs index 5627c834..367bf5ff 100644 --- a/src/packages/iter_basic.rs +++ b/src/packages/iter_basic.rs @@ -287,7 +287,7 @@ macro_rules! reg_range { "/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.", "/// The value `to` is never included.", "///", - "/// If `from` > `to` and `step` < 0, the iteration goes backwards.", + "/// If `from` > `to` and `step` < 0, iteration goes backwards.", "///", "/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.", "///", @@ -305,6 +305,35 @@ macro_rules! reg_range { "/// }", "/// ```" ]); + + let _hash = $lib.set_native_fn($x, |range: std::ops::Range<$y>, step: $y| StepRange::new(range.start, range.end, step, $add)); + + #[cfg(feature = "metadata")] + $lib.update_fn_metadata_with_comments(_hash, [ + concat!("range: Range<", stringify!($y), ">"), + concat!("step: ", stringify!($y)), + concat!("Iterator") + ], [ + "/// Return an iterator over an exclusive range, each iteration increasing by `step`.", + "///", + "/// If `range` is reversed and `step` < 0, iteration goes backwards.", + "///", + "/// Otherwise, if `range` is empty, an empty iterator is returned.", + "///", + "/// # Example", + "///", + "/// ```rhai", + "/// // prints all values from 8 to 17 in steps of 3", + "/// for n in range(8..18, 3) {", + "/// print(n);", + "/// }", + "///", + "/// // prints all values down from 18 to 9 in steps of -3", + "/// for n in range(18..8, -3) {", + "/// print(n);", + "/// }", + "/// ```" + ]); )* }; } From 107193e35f3ad18ea31dd5ac1b48cc98a0bcab30 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 17 Jul 2022 18:49:12 +0800 Subject: [PATCH 5/7] Update rustyline to 10. --- Cargo.toml | 4 ++-- src/bin/rhai-repl.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 684e2592..e571dc48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ serde = { version = "1.0", default-features = false, features = ["derive", "allo serde_json = { version = "1.0", default-features = false, features = ["alloc"], optional = true } unicode-xid = { version = "0.2", default-features = false, optional = true } rust_decimal = { version = "1.16", default-features = false, features = ["maths"], optional = true } -rustyline = { version = "9", optional = true } +rustyline = { version = "10", optional = true } [dev-dependencies] serde_bytes = "0.11" @@ -96,4 +96,4 @@ features = ["metadata", "serde", "internals", "decimal", "debugging"] [patch.crates-io] # Notice that a custom modified version of `rustyline` is used which supports bracketed paste on Windows. # This can be moved to the official version when bracketed paste is added. -rustyline = { git = "https://github.com/schungx/rustyline" } +rustyline = { git = "https://github.com/schungx/rustyline", branch = "v10" } diff --git a/src/bin/rhai-repl.rs b/src/bin/rhai-repl.rs index 0cf59289..4fe171ce 100644 --- a/src/bin/rhai-repl.rs +++ b/src/bin/rhai-repl.rs @@ -194,7 +194,7 @@ fn setup_editor() -> Editor<()> { .indent_size(4) .bracketed_paste(true) .build(); - let mut rl = Editor::<()>::with_config(config); + let mut rl = Editor::<()>::with_config(config).unwrap(); // Bind more keys From 4b760d1d0f5beff2f6b407b8329a7b2e81d933ed Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 18 Jul 2022 08:54:10 +0800 Subject: [PATCH 6/7] Unroll switch ranges if possible. --- CHANGELOG.md | 1 + src/ast/stmt.rs | 39 ++++++++++++--------- src/optimizer.rs | 88 ++++++++++++++++++++++++++++-------------------- src/parser.rs | 21 +++++++----- tests/switch.rs | 43 +++++++++++++++++++++++ 5 files changed, 132 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73f3a40c..e9e88417 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Enhancements * `switch` cases can now include multiple values separated by `|`. * `EvalContext::eval_expression_tree_raw` and `Expression::eval_with_context_raw` are added to allow for not rewinding the `Scope` at the end of a statements block. * A new `range` function variant that takes an exclusive range with a step. +* Ranges in `switch` statements that are small (currently no more than 16 items) are unrolled if possible. Version 1.8.0 diff --git a/src/ast/stmt.rs b/src/ast/stmt.rs index 5acbd892..0448be56 100644 --- a/src/ast/stmt.rs +++ b/src/ast/stmt.rs @@ -175,7 +175,6 @@ impl fmt::Debug for RangeCase { impl From> for RangeCase { #[inline(always)] - #[must_use] fn from(value: Range) -> Self { Self::ExclusiveInt(value, 0) } @@ -183,12 +182,24 @@ impl From> for RangeCase { impl From> for RangeCase { #[inline(always)] - #[must_use] fn from(value: RangeInclusive) -> Self { Self::InclusiveInt(value, 0) } } +impl IntoIterator for RangeCase { + type Item = INT; + type IntoIter = Box>; + + #[inline(always)] + fn into_iter(self) -> Self::IntoIter { + match self { + Self::ExclusiveInt(r, ..) => Box::new(r.into_iter()), + Self::InclusiveInt(r, ..) => Box::new(r.into_iter()), + } + } +} + impl RangeCase { /// Is the range empty? #[inline(always)] @@ -199,6 +210,17 @@ impl RangeCase { Self::InclusiveInt(r, ..) => r.is_empty(), } } + /// Size of the range. + #[inline(always)] + #[must_use] + pub fn len(&self) -> usize { + match self { + Self::ExclusiveInt(r, ..) if r.is_empty() => 0, + Self::ExclusiveInt(r, ..) => (r.end - r.start) as usize, + Self::InclusiveInt(r, ..) if r.is_empty() => 0, + Self::InclusiveInt(r, ..) => (*r.end() - *r.start()) as usize, + } + } /// Is the specified number within this range? #[inline(always)] #[must_use] @@ -208,19 +230,6 @@ impl RangeCase { Self::InclusiveInt(r, ..) => r.contains(&n), } } - /// If the range contains only of a single [`INT`], return it; - /// otherwise return [`None`]. - #[inline(always)] - #[must_use] - pub fn single_int(&self) -> Option { - match self { - Self::ExclusiveInt(r, ..) if r.end.checked_sub(r.start) == Some(1) => Some(r.start), - Self::InclusiveInt(r, ..) if r.end().checked_sub(*r.start()) == Some(0) => { - Some(*r.start()) - } - _ => None, - } - } /// Is the specified range inclusive? #[inline(always)] #[must_use] diff --git a/src/optimizer.rs b/src/optimizer.rs index 2a807055..3c55695e 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -525,7 +525,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b let ( match_expr, SwitchCases { - blocks: blocks_list, + blocks, cases, ranges, def_case, @@ -538,29 +538,29 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b let hash = hasher.finish(); // First check hashes - if let Some(block) = cases.remove(&hash) { - let mut block = mem::take(&mut blocks_list[block]); + if let Some(b) = cases.remove(&hash) { + let mut b = mem::take(&mut blocks[b]); cases.clear(); - match block.condition { + match b.condition { Expr::BoolConstant(true, ..) => { // Promote the matched case - let statements: StmtBlockContainer = mem::take(&mut block.statements); + let statements: StmtBlockContainer = mem::take(&mut b.statements); let statements = optimize_stmt_block(statements, state, true, true, false); - *stmt = (statements, block.statements.span()).into(); + *stmt = (statements, b.statements.span()).into(); } ref mut condition => { // switch const { case if condition => stmt, _ => def } => if condition { stmt } else { def } optimize_expr(condition, state, false); - let def_case = &mut blocks_list[*def_case].statements; + let def_case = &mut blocks[*def_case].statements; let def_span = def_case.span_or_else(*pos, Position::NONE); let def_case: StmtBlockContainer = mem::take(def_case); let def_stmt = optimize_stmt_block(def_case, state, true, true, false); *stmt = Stmt::If( ( mem::take(condition), - mem::take(&mut block.statements), + mem::take(&mut b.statements), StmtBlock::new_with_span(def_stmt, def_span), ) .into(), @@ -580,19 +580,16 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // Only one range or all ranges without conditions if ranges.len() == 1 || ranges.iter().all(|r| { - matches!( - blocks_list[r.index()].condition, - Expr::BoolConstant(true, ..) - ) + matches!(blocks[r.index()].condition, Expr::BoolConstant(true, ..)) }) { for r in ranges.iter().filter(|r| r.contains(value)) { - let condition = mem::take(&mut blocks_list[r.index()].condition); + let condition = mem::take(&mut blocks[r.index()].condition); match condition { Expr::BoolConstant(true, ..) => { // Promote the matched case - let block = &mut blocks_list[r.index()]; + let block = &mut blocks[r.index()]; let statements = mem::take(&mut *block.statements); let statements = optimize_stmt_block(statements, state, true, true, false); @@ -602,13 +599,13 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // switch const { range if condition => stmt, _ => def } => if condition { stmt } else { def } optimize_expr(&mut condition, state, false); - let def_case = &mut blocks_list[*def_case].statements; + let def_case = &mut blocks[*def_case].statements; let def_span = def_case.span_or_else(*pos, Position::NONE); let def_case: StmtBlockContainer = mem::take(def_case); let def_stmt = optimize_stmt_block(def_case, state, true, true, false); - let statements = mem::take(&mut blocks_list[r.index()].statements); + let statements = mem::take(&mut blocks[r.index()].statements); *stmt = Stmt::If( ( @@ -641,16 +638,16 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b } for r in &*ranges { - let block = &mut blocks_list[r.index()]; - let statements = mem::take(&mut *block.statements); - *block.statements = + let b = &mut blocks[r.index()]; + let statements = mem::take(&mut *b.statements); + *b.statements = optimize_stmt_block(statements, state, preserve_result, true, false); - optimize_expr(&mut block.condition, state, false); + optimize_expr(&mut b.condition, state, false); - match block.condition { + match b.condition { Expr::Unit(pos) => { - block.condition = Expr::BoolConstant(true, pos); + b.condition = Expr::BoolConstant(true, pos); state.set_dirty() } _ => (), @@ -662,7 +659,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // Promote the default case state.set_dirty(); - let def_case = &mut blocks_list[*def_case].statements; + let def_case = &mut blocks[*def_case].statements; let def_span = def_case.span_or_else(*pos, Position::NONE); let def_case: StmtBlockContainer = mem::take(def_case); let def_stmt = optimize_stmt_block(def_case, state, true, true, false); @@ -673,7 +670,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b let ( match_expr, SwitchCases { - blocks: blocks_list, + blocks, cases, ranges, def_case, @@ -684,21 +681,21 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b optimize_expr(match_expr, state, false); // Optimize blocks - for block in blocks_list.iter_mut() { - let statements = mem::take(&mut *block.statements); - *block.statements = + for b in blocks.iter_mut() { + let statements = mem::take(&mut *b.statements); + *b.statements = optimize_stmt_block(statements, state, preserve_result, true, false); - optimize_expr(&mut block.condition, state, false); + optimize_expr(&mut b.condition, state, false); - match block.condition { + match b.condition { Expr::Unit(pos) => { - block.condition = Expr::BoolConstant(true, pos); + b.condition = Expr::BoolConstant(true, pos); state.set_dirty(); } Expr::BoolConstant(false, ..) => { - if !block.statements.is_empty() { - block.statements = StmtBlock::NONE; + if !b.statements.is_empty() { + b.statements = StmtBlock::NONE; state.set_dirty(); } } @@ -707,7 +704,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b } // Remove false cases - cases.retain(|_, &mut block| match blocks_list[block].condition { + cases.retain(|_, &mut block| match blocks[block].condition { Expr::BoolConstant(false, ..) => { state.set_dirty(); false @@ -715,7 +712,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b _ => true, }); // Remove false ranges - ranges.retain(|r| match blocks_list[r.index()].condition { + ranges.retain(|r| match blocks[r.index()].condition { Expr::BoolConstant(false, ..) => { state.set_dirty(); false @@ -723,9 +720,26 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b _ => true, }); - let def_case = &mut blocks_list[*def_case].statements; - let def_block = mem::take(&mut **def_case); - **def_case = optimize_stmt_block(def_block, state, preserve_result, true, false); + let def_stmt_block = &mut blocks[*def_case].statements; + let def_block = mem::take(&mut **def_stmt_block); + **def_stmt_block = optimize_stmt_block(def_block, state, preserve_result, true, false); + + // Remove unused block statements + for index in 0..blocks.len() { + if *def_case == index + || cases.values().any(|&n| n == index) + || ranges.iter().any(|r| r.index() == index) + { + continue; + } + + let b = &mut blocks[index]; + + if !b.statements.is_empty() { + b.statements = StmtBlock::NONE; + state.set_dirty(); + } + } } // while false { block } -> Noop diff --git a/src/parser.rs b/src/parser.rs index 119f207d..a59a2e81 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -39,6 +39,9 @@ const SCOPE_SEARCH_BARRIER_MARKER: &str = "$ BARRIER $"; /// The message: `TokenStream` never ends const NEVER_ENDS: &str = "`Token`"; +/// Unroll `switch` ranges no larger than this. +const SMALL_SWITCH_RANGE: usize = 16; + /// _(internals)_ A type that encapsulates the current state of the parser. /// Exported under the `internals` feature only. pub struct ParseState<'e> { @@ -1138,6 +1141,7 @@ impl Engine { let stmt = self.parse_stmt(input, state, lib, settings.level_up())?; let need_comma = !stmt.is_self_terminated(); + let has_condition = !matches!(condition, Expr::BoolConstant(true, ..)); blocks.push((condition, stmt).into()); let index = blocks.len() - 1; @@ -1159,14 +1163,15 @@ impl Engine { if let Some(mut r) = range_value { if !r.is_empty() { - if let Some(n) = r.single_int() { - // Unroll single range - let value = Dynamic::from_int(n); - let hasher = &mut get_hasher(); - value.hash(hasher); - let hash = hasher.finish(); - - cases.entry(hash).or_insert(index); + // Do not unroll ranges if there are previous non-unrolled ranges + if !has_condition && ranges.is_empty() && r.len() <= SMALL_SWITCH_RANGE + { + // Unroll small range + for n in r { + let hasher = &mut get_hasher(); + Dynamic::from_int(n).hash(hasher); + cases.entry(hasher.finish()).or_insert(index); + } } else { // Other range r.set_index(index); diff --git a/tests/switch.rs b/tests/switch.rs index 301401a8..5d7f18fa 100644 --- a/tests/switch.rs +++ b/tests/switch.rs @@ -290,6 +290,49 @@ fn test_switch_ranges() -> Result<(), Box> { )?, 'x' ); + assert_eq!( + engine.eval_with_scope::( + &mut scope, + " + switch 5 { + 'a' => true, + 0..10 => 123, + 2..12 => 'z', + _ => 'x' + } + " + )?, + 123 + ); + assert_eq!( + engine.eval_with_scope::( + &mut scope, + " + switch 5 { + 'a' => true, + 4 | 5 | 6 => 42, + 0..10 => 123, + 2..12 => 'z', + _ => 'x' + } + " + )?, + 42 + ); + assert_eq!( + engine.eval_with_scope::( + &mut scope, + " + switch 5 { + 'a' => true, + 2..12 => 'z', + 0..10 if x+2==1+2 => print(40+2), + _ => 'x' + } + " + )?, + 'z' + ); assert_eq!( engine.eval_with_scope::( &mut scope, From 7dca916c451279a6fcb760a1b6ac68e65a0fb359 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 18 Jul 2022 13:40:41 +0800 Subject: [PATCH 7/7] Allow duplicated switch cases. --- CHANGELOG.md | 9 ++- src/ast/mod.rs | 4 +- src/ast/stmt.rs | 45 +++++++----- src/eval/stmt.rs | 59 ++++++++++------ src/lib.rs | 2 +- src/optimizer.rs | 149 +++++++++++++++++++++++++-------------- src/parser.rs | 52 +++++++------- src/types/parse_error.rs | 9 +++ tests/switch.rs | 51 +++++++------- 9 files changed, 235 insertions(+), 145 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9e88417..bc16d51d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,17 @@ New features Enhancements ------------ +### `switch` statement + * `switch` cases can now include multiple values separated by `|`. +* Duplicated `switch` cases are now allowed. +* The error `ParseErrorType::DuplicatedSwitchCase` is deprecated. +* Ranges in `switch` statements that are small (currently no more than 16 items) are unrolled if possible. + +### Others + * `EvalContext::eval_expression_tree_raw` and `Expression::eval_with_context_raw` are added to allow for not rewinding the `Scope` at the end of a statements block. * A new `range` function variant that takes an exclusive range with a step. -* Ranges in `switch` statements that are small (currently no more than 16 items) are unrolled if possible. Version 1.8.0 diff --git a/src/ast/mod.rs b/src/ast/mod.rs index cb525116..8d9cacf2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -22,8 +22,8 @@ pub use script_fn::EncapsulatedEnviron; #[cfg(not(feature = "no_function"))] pub use script_fn::{ScriptFnDef, ScriptFnMetadata}; pub use stmt::{ - ConditionalStmtBlock, OpAssignment, RangeCase, Stmt, StmtBlock, StmtBlockContainer, - SwitchCases, TryCatchBlock, + CaseBlocksList, ConditionalStmtBlock, OpAssignment, RangeCase, Stmt, StmtBlock, + StmtBlockContainer, SwitchCasesCollection, TryCatchBlock, }; #[cfg(not(feature = "no_float"))] diff --git a/src/ast/stmt.rs b/src/ast/stmt.rs index 0448be56..153ee2ad 100644 --- a/src/ast/stmt.rs +++ b/src/ast/stmt.rs @@ -176,14 +176,14 @@ impl fmt::Debug for RangeCase { impl From> for RangeCase { #[inline(always)] fn from(value: Range) -> Self { - Self::ExclusiveInt(value, 0) + Self::ExclusiveInt(value, usize::MAX) } } impl From> for RangeCase { #[inline(always)] fn from(value: RangeInclusive) -> Self { - Self::InclusiveInt(value, 0) + Self::InclusiveInt(value, usize::MAX) } } @@ -256,14 +256,16 @@ impl RangeCase { } } +pub type CaseBlocksList = smallvec::SmallVec<[usize; 1]>; + /// _(internals)_ A type containing all cases for a `switch` statement. /// Exported under the `internals` feature only. #[derive(Debug, Clone, Hash)] -pub struct SwitchCases { +pub struct SwitchCasesCollection { /// List of [`ConditionalStmtBlock`]'s. - pub blocks: StaticVec, + pub case_blocks: StaticVec, /// Dictionary mapping value hashes to [`ConditionalStmtBlock`]'s. - pub cases: BTreeMap, + pub cases: BTreeMap, /// Statements block for the default case (there can be no condition for the default case). pub def_case: usize, /// List of range cases. @@ -512,7 +514,7 @@ pub enum Stmt { /// 0) Hash table for (condition, block) /// 1) Default block /// 2) List of ranges: (start, end, inclusive, condition, statement) - Switch(Box<(Expr, SwitchCases)>, Position), + Switch(Box<(Expr, SwitchCasesCollection)>, Position), /// `while` expr `{` stmt `}` | `loop` `{` stmt `}` /// /// If the guard expression is [`UNIT`][Expr::Unit], then it is a `loop` statement. @@ -755,15 +757,18 @@ impl Stmt { Self::Switch(x, ..) => { let (expr, sw) = &**x; expr.is_pure() - && sw.cases.values().all(|&c| { - let block = &sw.blocks[c]; + && sw.cases.values().flat_map(|cases| cases.iter()).all(|&c| { + let block = &sw.case_blocks[c]; block.condition.is_pure() && block.statements.iter().all(Stmt::is_pure) }) && sw.ranges.iter().all(|r| { - let block = &sw.blocks[r.index()]; + let block = &sw.case_blocks[r.index()]; block.condition.is_pure() && block.statements.iter().all(Stmt::is_pure) }) - && sw.blocks[sw.def_case].statements.iter().all(Stmt::is_pure) + && sw.case_blocks[sw.def_case] + .statements + .iter() + .all(Stmt::is_pure) } // Loops that exit can be pure because it can never be infinite. @@ -904,20 +909,22 @@ impl Stmt { if !expr.walk(path, on_node) { return false; } - for (.., &b) in &sw.cases { - let block = &sw.blocks[b]; + for (.., blocks) in &sw.cases { + for &b in blocks { + let block = &sw.case_blocks[b]; - if !block.condition.walk(path, on_node) { - return false; - } - for s in &block.statements { - if !s.walk(path, on_node) { + if !block.condition.walk(path, on_node) { return false; } + for s in &block.statements { + if !s.walk(path, on_node) { + return false; + } + } } } for r in &sw.ranges { - let block = &sw.blocks[r.index()]; + let block = &sw.case_blocks[r.index()]; if !block.condition.walk(path, on_node) { return false; @@ -928,7 +935,7 @@ impl Stmt { } } } - for s in &sw.blocks[sw.def_case].statements { + for s in &sw.case_blocks[sw.def_case].statements { if !s.walk(path, on_node) { return false; } diff --git a/src/eval/stmt.rs b/src/eval/stmt.rs index 1fcef61f..03d36097 100644 --- a/src/eval/stmt.rs +++ b/src/eval/stmt.rs @@ -3,7 +3,7 @@ use super::{Caches, EvalContext, GlobalRuntimeState, Target}; use crate::api::events::VarDefInfo; use crate::ast::{ - ASTFlags, BinaryExpr, Expr, Ident, OpAssignment, Stmt, SwitchCases, TryCatchBlock, + ASTFlags, BinaryExpr, Expr, Ident, OpAssignment, Stmt, SwitchCasesCollection, TryCatchBlock, }; use crate::func::get_hasher; use crate::types::dynamic::{AccessMode, Union}; @@ -393,8 +393,8 @@ impl Engine { Stmt::Switch(x, ..) => { let ( expr, - SwitchCases { - blocks, + SwitchCasesCollection { + case_blocks, cases, def_case, ranges, @@ -411,32 +411,49 @@ impl Engine { let hash = hasher.finish(); // First check hashes - if let Some(&case_block) = cases.get(&hash) { - let case_block = &blocks[case_block]; + if let Some(case_blocks_list) = cases.get(&hash) { + assert!(!case_blocks_list.is_empty()); - let cond_result = match case_block.condition { - Expr::BoolConstant(b, ..) => Ok(b), - ref c => self - .eval_expr(scope, global, caches, lib, this_ptr, c, level) - .and_then(|v| { - v.as_bool().map_err(|typ| { - self.make_type_mismatch_err::(typ, c.position()) - }) - }), - }; + let mut result = Ok(None); - match cond_result { - Ok(true) => Ok(Some(&case_block.statements)), - Ok(false) => Ok(None), - _ => cond_result.map(|_| None), + for &index in case_blocks_list { + let block = &case_blocks[index]; + + let cond_result = match block.condition { + Expr::BoolConstant(b, ..) => Ok(b), + ref c => self + .eval_expr(scope, global, caches, lib, this_ptr, c, level) + .and_then(|v| { + v.as_bool().map_err(|typ| { + self.make_type_mismatch_err::( + typ, + c.position(), + ) + }) + }), + }; + + match cond_result { + Ok(true) => { + result = Ok(Some(&block.statements)); + break; + } + Ok(false) => (), + _ => { + result = cond_result.map(|_| None); + break; + } + } } + + result } else if value.is::() && !ranges.is_empty() { // Then check integer ranges let value = value.as_int().expect("`INT`"); let mut result = Ok(None); for r in ranges.iter().filter(|r| r.contains(value)) { - let block = &blocks[r.index()]; + let block = &case_blocks[r.index()]; let cond_result = match block.condition { Expr::BoolConstant(b, ..) => Ok(b), @@ -481,7 +498,7 @@ impl Engine { } } else if let Ok(None) = stmt_block_result { // Default match clause - let def_case = &blocks[*def_case].statements; + let def_case = &case_blocks[*def_case].statements; if !def_case.is_empty() { self.eval_stmt_block( diff --git a/src/lib.rs b/src/lib.rs index c24ab2bc..915b9966 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -285,7 +285,7 @@ pub use parser::ParseState; #[cfg(feature = "internals")] pub use ast::{ ASTFlags, ASTNode, BinaryExpr, ConditionalStmtBlock, Expr, FnCallExpr, FnCallHashes, Ident, - OpAssignment, RangeCase, ScriptFnDef, Stmt, StmtBlock, SwitchCases, TryCatchBlock, + OpAssignment, RangeCase, ScriptFnDef, Stmt, StmtBlock, SwitchCasesCollection, TryCatchBlock, }; #[cfg(feature = "internals")] diff --git a/src/optimizer.rs b/src/optimizer.rs index 3c55695e..794cc124 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -1,7 +1,9 @@ //! Module implementing the [`AST`] optimizer. #![cfg(not(feature = "no_optimize"))] -use crate::ast::{ASTFlags, Expr, OpAssignment, Stmt, StmtBlock, StmtBlockContainer, SwitchCases}; +use crate::ast::{ + ASTFlags, Expr, OpAssignment, Stmt, StmtBlock, StmtBlockContainer, SwitchCasesCollection, +}; use crate::engine::{KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_PRINT, KEYWORD_TYPE_OF}; use crate::eval::{Caches, GlobalRuntimeState}; use crate::func::builtin::get_builtin_binary_op_fn; @@ -524,8 +526,8 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b Stmt::Switch(x, pos) if x.0.is_constant() => { let ( match_expr, - SwitchCases { - blocks, + SwitchCasesCollection { + case_blocks, cases, ranges, def_case, @@ -538,39 +540,65 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b let hash = hasher.finish(); // First check hashes - if let Some(b) = cases.remove(&hash) { - let mut b = mem::take(&mut blocks[b]); - cases.clear(); + if let Some(case_blocks_list) = cases.get(&hash) { + match &case_blocks_list[..] { + [] => (), + [index] => { + let mut b = mem::take(&mut case_blocks[*index]); + cases.clear(); - match b.condition { - Expr::BoolConstant(true, ..) => { - // Promote the matched case - let statements: StmtBlockContainer = mem::take(&mut b.statements); - let statements = optimize_stmt_block(statements, state, true, true, false); - *stmt = (statements, b.statements.span()).into(); + match b.condition { + Expr::BoolConstant(true, ..) => { + // Promote the matched case + let statements: StmtBlockContainer = mem::take(&mut b.statements); + let statements = + optimize_stmt_block(statements, state, true, true, false); + *stmt = (statements, b.statements.span()).into(); + } + ref mut condition => { + // switch const { case if condition => stmt, _ => def } => if condition { stmt } else { def } + optimize_expr(condition, state, false); + + let def_case = &mut case_blocks[*def_case].statements; + let def_span = def_case.span_or_else(*pos, Position::NONE); + let def_case: StmtBlockContainer = mem::take(def_case); + let def_stmt = + optimize_stmt_block(def_case, state, true, true, false); + *stmt = Stmt::If( + ( + mem::take(condition), + mem::take(&mut b.statements), + StmtBlock::new_with_span(def_stmt, def_span), + ) + .into(), + match_expr.start_position(), + ); + } + } + + state.set_dirty(); + return; } - ref mut condition => { - // switch const { case if condition => stmt, _ => def } => if condition { stmt } else { def } - optimize_expr(condition, state, false); + _ => { + for &index in case_blocks_list { + let mut b = mem::take(&mut case_blocks[index]); - let def_case = &mut blocks[*def_case].statements; - let def_span = def_case.span_or_else(*pos, Position::NONE); - let def_case: StmtBlockContainer = mem::take(def_case); - let def_stmt = optimize_stmt_block(def_case, state, true, true, false); - *stmt = Stmt::If( - ( - mem::take(condition), - mem::take(&mut b.statements), - StmtBlock::new_with_span(def_stmt, def_span), - ) - .into(), - match_expr.start_position(), - ); + match b.condition { + Expr::BoolConstant(true, ..) => { + // Promote the matched case + let statements: StmtBlockContainer = + mem::take(&mut b.statements); + let statements = + optimize_stmt_block(statements, state, true, true, false); + *stmt = (statements, b.statements.span()).into(); + state.set_dirty(); + return; + } + _ => (), + } + } } } - - state.set_dirty(); - return; } // Then check ranges @@ -580,16 +608,19 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // Only one range or all ranges without conditions if ranges.len() == 1 || ranges.iter().all(|r| { - matches!(blocks[r.index()].condition, Expr::BoolConstant(true, ..)) + matches!( + case_blocks[r.index()].condition, + Expr::BoolConstant(true, ..) + ) }) { for r in ranges.iter().filter(|r| r.contains(value)) { - let condition = mem::take(&mut blocks[r.index()].condition); + let condition = mem::take(&mut case_blocks[r.index()].condition); match condition { Expr::BoolConstant(true, ..) => { // Promote the matched case - let block = &mut blocks[r.index()]; + let block = &mut case_blocks[r.index()]; let statements = mem::take(&mut *block.statements); let statements = optimize_stmt_block(statements, state, true, true, false); @@ -599,13 +630,13 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // switch const { range if condition => stmt, _ => def } => if condition { stmt } else { def } optimize_expr(&mut condition, state, false); - let def_case = &mut blocks[*def_case].statements; + let def_case = &mut case_blocks[*def_case].statements; let def_span = def_case.span_or_else(*pos, Position::NONE); let def_case: StmtBlockContainer = mem::take(def_case); let def_stmt = optimize_stmt_block(def_case, state, true, true, false); - let statements = mem::take(&mut blocks[r.index()].statements); + let statements = mem::take(&mut case_blocks[r.index()].statements); *stmt = Stmt::If( ( @@ -638,7 +669,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b } for r in &*ranges { - let b = &mut blocks[r.index()]; + let b = &mut case_blocks[r.index()]; let statements = mem::take(&mut *b.statements); *b.statements = optimize_stmt_block(statements, state, preserve_result, true, false); @@ -659,7 +690,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // Promote the default case state.set_dirty(); - let def_case = &mut blocks[*def_case].statements; + let def_case = &mut case_blocks[*def_case].statements; let def_span = def_case.span_or_else(*pos, Position::NONE); let def_case: StmtBlockContainer = mem::take(def_case); let def_stmt = optimize_stmt_block(def_case, state, true, true, false); @@ -669,8 +700,8 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b Stmt::Switch(x, ..) => { let ( match_expr, - SwitchCases { - blocks, + SwitchCasesCollection { + case_blocks, cases, ranges, def_case, @@ -681,7 +712,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b optimize_expr(match_expr, state, false); // Optimize blocks - for b in blocks.iter_mut() { + for b in case_blocks.iter_mut() { let statements = mem::take(&mut *b.statements); *b.statements = optimize_stmt_block(statements, state, preserve_result, true, false); @@ -704,15 +735,31 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b } // Remove false cases - cases.retain(|_, &mut block| match blocks[block].condition { - Expr::BoolConstant(false, ..) => { - state.set_dirty(); - false + let cases_len = cases.len(); + cases.retain(|_, list| { + // Remove all entries that have false conditions + list.retain(|index| match case_blocks[*index].condition { + Expr::BoolConstant(false, ..) => false, + _ => true, + }); + // Remove all entries after a `true` condition + if let Some(n) = list + .iter() + .find(|&&index| match case_blocks[index].condition { + Expr::BoolConstant(true, ..) => true, + _ => false, + }) + { + list.truncate(n + 1); } - _ => true, + // Remove if no entry left + !list.is_empty() }); + if cases.len() != cases_len { + state.set_dirty(); + } // Remove false ranges - ranges.retain(|r| match blocks[r.index()].condition { + ranges.retain(|r| match case_blocks[r.index()].condition { Expr::BoolConstant(false, ..) => { state.set_dirty(); false @@ -720,20 +767,20 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b _ => true, }); - let def_stmt_block = &mut blocks[*def_case].statements; + let def_stmt_block = &mut case_blocks[*def_case].statements; let def_block = mem::take(&mut **def_stmt_block); **def_stmt_block = optimize_stmt_block(def_block, state, preserve_result, true, false); // Remove unused block statements - for index in 0..blocks.len() { + for index in 0..case_blocks.len() { if *def_case == index - || cases.values().any(|&n| n == index) + || cases.values().flat_map(|c| c.iter()).any(|&n| n == index) || ranges.iter().any(|r| r.index() == index) { continue; } - let b = &mut blocks[index]; + let b = &mut case_blocks[index]; if !b.statements.is_empty() { b.statements = StmtBlock::NONE; diff --git a/src/parser.rs b/src/parser.rs index a59a2e81..0f36dd2b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3,8 +3,9 @@ use crate::api::events::VarDefInfo; use crate::api::options::LangOptions; use crate::ast::{ - ASTFlags, BinaryExpr, ConditionalStmtBlock, Expr, FnCallExpr, FnCallHashes, Ident, - OpAssignment, RangeCase, ScriptFnDef, Stmt, StmtBlockContainer, SwitchCases, TryCatchBlock, + ASTFlags, BinaryExpr, CaseBlocksList, ConditionalStmtBlock, Expr, FnCallExpr, FnCallHashes, + Ident, OpAssignment, RangeCase, ScriptFnDef, Stmt, StmtBlockContainer, SwitchCasesCollection, + TryCatchBlock, }; use crate::engine::{Precedence, KEYWORD_THIS, OP_CONTAINS}; use crate::eval::GlobalRuntimeState; @@ -1054,11 +1055,11 @@ impl Engine { } } - let mut blocks = StaticVec::::new(); - let mut cases = BTreeMap::::new(); + let mut case_blocks = StaticVec::::new(); + let mut cases = BTreeMap::::new(); let mut ranges = StaticVec::::new(); - let mut def_pos = Position::NONE; - let mut def_stmt = None; + let mut def_stmt_pos = Position::NONE; + let mut def_stmt_index = None; loop { const MISSING_RBRACE: &str = "to end this switch block"; @@ -1074,8 +1075,8 @@ impl Engine { .into_err(*pos), ) } - (Token::Underscore, pos) if def_stmt.is_none() => { - def_pos = *pos; + (Token::Underscore, pos) if def_stmt_index.is_none() => { + def_stmt_pos = *pos; eat_token(input, Token::Underscore); let (if_clause, if_pos) = match_token(input, Token::If); @@ -1086,10 +1087,8 @@ impl Engine { (Default::default(), Expr::BoolConstant(true, Position::NONE)) } - (Token::Underscore, pos) => return Err(PERR::DuplicatedSwitchCase.into_err(*pos)), - - _ if def_stmt.is_some() => { - return Err(PERR::WrongSwitchDefaultCase.into_err(def_pos)) + _ if def_stmt_index.is_some() => { + return Err(PERR::WrongSwitchDefaultCase.into_err(def_stmt_pos)) } _ => { @@ -1143,8 +1142,8 @@ impl Engine { let need_comma = !stmt.is_self_terminated(); let has_condition = !matches!(condition, Expr::BoolConstant(true, ..)); - blocks.push((condition, stmt).into()); - let index = blocks.len() - 1; + case_blocks.push((condition, stmt).into()); + let index = case_blocks.len() - 1; if !case_expr_list.is_empty() { for expr in case_expr_list { @@ -1170,7 +1169,10 @@ impl Engine { for n in r { let hasher = &mut get_hasher(); Dynamic::from_int(n).hash(hasher); - cases.entry(hasher.finish()).or_insert(index); + cases + .entry(hasher.finish()) + .and_modify(|cases| cases.push(index)) + .or_insert_with(|| [index].into()); } } else { // Other range @@ -1189,13 +1191,13 @@ impl Engine { value.hash(hasher); let hash = hasher.finish(); - if cases.contains_key(&hash) { - return Err(PERR::DuplicatedSwitchCase.into_err(expr.start_position())); - } - cases.insert(hash, index); + cases + .entry(hash) + .and_modify(|cases| cases.push(index)) + .or_insert_with(|| [index].into()); } } else { - def_stmt = Some(index); + def_stmt_index = Some(index); } match input.peek().expect(NEVER_ENDS) { @@ -1221,13 +1223,13 @@ impl Engine { } } - let def_case = def_stmt.unwrap_or_else(|| { - blocks.push(Default::default()); - blocks.len() - 1 + let def_case = def_stmt_index.unwrap_or_else(|| { + case_blocks.push(Default::default()); + case_blocks.len() - 1 }); - let cases = SwitchCases { - blocks, + let cases = SwitchCasesCollection { + case_blocks, cases, def_case, ranges, diff --git a/src/types/parse_error.rs b/src/types/parse_error.rs index 266ddc34..4278ec5c 100644 --- a/src/types/parse_error.rs +++ b/src/types/parse_error.rs @@ -97,6 +97,14 @@ pub enum ParseErrorType { /// A map definition has duplicated property names. Wrapped value is the property name. DuplicatedProperty(String), /// A `switch` case is duplicated. + /// + /// # Deprecated + /// + /// This error variant is deprecated. It never occurs and will be removed in the next major version. + #[deprecated( + since = "1.9.0", + note = "This error variant is deprecated. It never occurs and will be removed in the next major version." + )] DuplicatedSwitchCase, /// A variable name is duplicated. Wrapped value is the variable name. DuplicatedVariable(String), @@ -211,6 +219,7 @@ impl fmt::Display for ParseErrorType { Self::FnDuplicatedParam(s, arg) => write!(f, "Duplicated parameter {} for function {}", arg, s), Self::DuplicatedProperty(s) => write!(f, "Duplicated property for object map literal: {}", s), + #[allow(deprecated)] Self::DuplicatedSwitchCase => f.write_str("Duplicated switch case"), Self::DuplicatedVariable(s) => write!(f, "Duplicated variable name: {}", s), diff --git a/tests/switch.rs b/tests/switch.rs index 5d7f18fa..edcc4546 100644 --- a/tests/switch.rs +++ b/tests/switch.rs @@ -32,7 +32,6 @@ fn test_switch() -> Result<(), Box> { )?, 'a' ); - assert_eq!( engine.eval_with_scope::(&mut scope, "switch x { 1 => (), 2 => 'a', 42 => true }")?, true @@ -98,6 +97,16 @@ fn test_switch() -> Result<(), Box> { 3 ); + assert_eq!( + engine.eval_with_scope::(&mut scope, "switch 42 { 42 => 123, 42 => 999 }")?, + 123 + ); + + assert_eq!( + engine.eval_with_scope::(&mut scope, "switch x { 42 => 123, 42 => 999 }")?, + 123 + ); + Ok(()) } @@ -105,13 +114,6 @@ fn test_switch() -> Result<(), Box> { fn test_switch_errors() -> Result<(), Box> { let engine = Engine::new(); - assert!(matches!( - *engine - .compile("switch x { 1 => 123, 1 => 42 }") - .expect_err("should error") - .0, - ParseErrorType::DuplicatedSwitchCase - )); assert!(matches!( *engine .compile("switch x { _ => 123, 1 => 42 }") @@ -159,23 +161,22 @@ fn test_switch_condition() -> Result<(), Box> { 9 ); - assert!(matches!( - *engine - .compile( - " - switch x { - 21 if x < 40 => 1, - 21 if x == 10 => 10, - 0 if x < 100 => 2, - 1 => 3, - _ => 9 - } - " - ) - .expect_err("should error") - .0, - ParseErrorType::DuplicatedSwitchCase - )); + assert_eq!( + engine.eval_with_scope::( + &mut scope, + " + switch x { + 42 if x < 40 => 1, + 42 if x > 40 => 7, + 0 if x < 100 => 2, + 1 => 3, + 42 if x == 10 => 10, + _ => 9 + } + " + )?, + 7 + ); assert!(matches!( *engine