diff --git a/CHANGELOG.md b/CHANGELOG.md index fc71773b..bff3b748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Breaking changes ---------------- * Negative index to an array or string yields the appropriate element/character counting from the _end_. +* The default `_` case of a `switch` statement now must be the last case, together with two new error variants: `EvalAltResult::WrongSwitchDefaultCase` and `EvalAltResult::WrongSwitchCaseCondition`. * `ModuleResolver` trait methods take an additional parameter `source_path` that contains the path of the current environment. This is to facilitate loading other script files always from the current directory. * `FileModuleResolver` now resolves relative paths under the source path if there is no base path set. * `FileModuleResolver::base_path` now returns `Option<&str>` which is `None` if there is no base path set. @@ -34,6 +35,7 @@ New features * String interpolation support is added via the `` `... ${`` ... ``} ...` `` syntax. * `FileModuleResolver` resolves relative paths under the parent path (i.e. the path holding the script that does the loading). This allows seamless cross-loading of scripts from a directory hierarchy instead of having all relative paths load from the current working directory. * Negative index to an array or string yields the appropriate element/character counting from the _end_. +* `switch` statement cases can now have an optional `if` clause. Version 0.19.15 diff --git a/scripts/README.md b/scripts/README.md index f3bf5b4a..d8a3bcb3 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -7,14 +7,8 @@ Testing scripts written in Rhai. How to Run ---------- -Compile the `rhai-run` example: +Run scripts using the `rhai-run` tool: -```bash -cargo build --example rhai-run -``` - -Run it: - -```bash -./target/debug/examples/rhai-run ./scripts/test_script_to_run.rhai +```sh +cargo run --bin rhai-run ./scripts/test_script_to_run.rhai ``` diff --git a/scripts/comments.rhai b/scripts/comments.rhai index dd3612fa..63b9f048 100644 --- a/scripts/comments.rhai +++ b/scripts/comments.rhai @@ -8,4 +8,4 @@ let /* I am a spy in a variable declaration! */ x = 5; /* look /* at /* that, /* multi-line */ comments */ can be */ nested */ -/* surrounded by */ this_is_not_a_comment = true // comments +/* surrounded by */ let this_is_not_a_comment = true // comments diff --git a/scripts/fibonacci.rhai b/scripts/fibonacci.rhai index fb40c64a..0787a350 100644 --- a/scripts/fibonacci.rhai +++ b/scripts/fibonacci.rhai @@ -1,7 +1,8 @@ // This script calculates the n-th Fibonacci number using a really dumb algorithm // to test the speed of the scripting engine. -const target = 28; +const TARGET = 28; +const REPEAT = 5; fn fib(n) { if n < 2 { @@ -11,19 +12,19 @@ fn fib(n) { } } -print("Running Fibonacci(28) x 5 times..."); +print(`Running Fibonacci(28) x ${REPEAT} times...`); print("Ready... Go!"); let result; let now = timestamp(); -for n in range(0, 5) { - result = fib(target); +for n in range(0, REPEAT) { + result = fib(TARGET); } print(`Finished. Run time = ${now.elapsed} seconds.`); -print(`Fibonacci number #${target} = ${result}`); +print(`Fibonacci number #${TARGET} = ${result}`); if result != 317_811 { print("The answer is WRONG! Should be 317,811!"); diff --git a/scripts/function_decl2.rhai b/scripts/function_decl2.rhai index ae14c454..99507209 100644 --- a/scripts/function_decl2.rhai +++ b/scripts/function_decl2.rhai @@ -9,6 +9,6 @@ fn addme(a, b) { let result = addme(a, 4); -print(!addme(a, 4) should be 46: ${result}``); +print(`addme(a, 4) should be 46: ${result}`); print(`a should still be 3: ${a}`); // should print 3 - 'a' is never changed diff --git a/src/ast.rs b/src/ast.rs index 074021bf..c25014a7 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -10,7 +10,7 @@ use crate::stdlib::{ hash::Hash, iter::empty, num::{NonZeroU8, NonZeroUsize}, - ops::{Add, AddAssign}, + ops::{Add, AddAssign, Deref, DerefMut}, vec, vec::Vec, }; @@ -187,10 +187,7 @@ impl AST { ) -> Self { Self { source: None, - body: StmtBlock { - statements: statements.into_iter().collect(), - pos: Position::NONE, - }, + body: StmtBlock(statements.into_iter().collect(), Position::NONE), functions: functions.into(), #[cfg(not(feature = "no_module"))] resolver: None, @@ -205,10 +202,7 @@ impl AST { ) -> Self { Self { source: Some(source.into()), - body: StmtBlock { - statements: statements.into_iter().collect(), - pos: Position::NONE, - }, + body: StmtBlock(statements.into_iter().collect(), Position::NONE), functions: functions.into(), #[cfg(not(feature = "no_module"))] resolver: None, @@ -245,7 +239,7 @@ impl AST { #[cfg(not(feature = "internals"))] #[inline(always)] pub(crate) fn statements(&self) -> &[Stmt] { - &self.body.statements + &self.body.0 } /// _(INTERNALS)_ Get the statements. /// Exported under the `internals` feature only. @@ -253,13 +247,13 @@ impl AST { #[deprecated = "this method is volatile and may change"] #[inline(always)] pub fn statements(&self) -> &[Stmt] { - &self.body.statements + &self.body.0 } /// Get a mutable reference to the statements. #[cfg(not(feature = "no_optimize"))] #[inline(always)] pub(crate) fn statements_mut(&mut self) -> &mut StaticVec { - &mut self.body.statements + &mut self.body.0 } /// Get the internal shared [`Module`] containing all script-defined functions. #[cfg(not(feature = "internals"))] @@ -535,8 +529,7 @@ impl AST { let merged = match (body.is_empty(), other.body.is_empty()) { (false, false) => { let mut body = body.clone(); - body.statements - .extend(other.body.statements.iter().cloned()); + body.0.extend(other.body.0.iter().cloned()); body } (false, true) => body.clone(), @@ -550,9 +543,9 @@ impl AST { functions.merge_filtered(&other.functions, &filter); if let Some(source) = source { - Self::new_with_source(merged.statements, functions, source) + Self::new_with_source(merged.0, functions, source) } else { - Self::new(merged.statements, functions) + Self::new(merged.0, functions) } } /// Combine one [`AST`] with another. The second [`AST`] is consumed. @@ -612,9 +605,7 @@ impl AST { other: Self, filter: impl Fn(FnNamespace, FnAccess, bool, &str, usize) -> bool, ) -> &mut Self { - self.body - .statements - .extend(other.body.statements.into_iter()); + self.body.0.extend(other.body.0.into_iter()); if !other.functions.is_empty() { shared_make_mut(&mut self.functions).merge_filtered(&other.functions, &filter); @@ -705,7 +696,7 @@ impl AST { } } #[cfg(not(feature = "no_function"))] - for stmt in self.iter_fn_def().flat_map(|f| f.body.statements.iter()) { + for stmt in self.iter_fn_def().flat_map(|f| f.body.0.iter()) { if !stmt.walk(path, on_node) { return false; } @@ -727,7 +718,7 @@ impl AST { } } #[cfg(not(feature = "no_function"))] - for stmt in self.iter_fn_def().flat_map(|f| f.body.statements.iter()) { + for stmt in self.iter_fn_def().flat_map(|f| f.body.0.iter()) { if !stmt.walk(path, on_node) { return false; } @@ -833,35 +824,65 @@ impl<'a> From<&'a Expr> for ASTNode<'a> { /// /// This type is volatile and may change. #[derive(Clone, Hash, Default)] -pub struct StmtBlock { - pub statements: StaticVec, - pub pos: Position, -} +pub struct StmtBlock(StaticVec, Position); impl StmtBlock { + /// Create a new [`StmtBlock`]. + pub fn new(statements: impl Into>, pos: Position) -> Self { + Self(statements.into(), pos) + } /// Is this statements block empty? #[inline(always)] pub fn is_empty(&self) -> bool { - self.statements.is_empty() + self.0.is_empty() } /// Number of statements in this statements block. #[inline(always)] pub fn len(&self) -> usize { - self.statements.len() + self.0.len() + } + /// Get the position of this statements block. + pub fn position(&self) -> Position { + self.1 + } + /// Get the statements of this statements block. + pub fn statements(&mut self) -> &mut StaticVec { + &mut self.0 + } +} + +impl Deref for StmtBlock { + type Target = StaticVec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for StmtBlock { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } } impl fmt::Debug for StmtBlock { #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self.statements, f)?; - if !self.pos.is_none() { - write!(f, " @ {:?}", self.pos)?; + fmt::Debug::fmt(&self.0, f)?; + if !self.1.is_none() { + write!(f, " @ {:?}", self.1)?; } Ok(()) } } +impl From for Stmt { + fn from(block: StmtBlock) -> Self { + let block_pos = block.position(); + Self::Block(block.0.into_vec(), block_pos) + } +} + /// _(INTERNALS)_ A statement. /// Exported under the `internals` feature only. /// @@ -874,10 +895,10 @@ pub enum Stmt { Noop(Position), /// `if` expr `{` stmt `}` `else` `{` stmt `}` If(Expr, Box<(StmtBlock, StmtBlock)>, Position), - /// `switch` expr `{` literal or _ `=>` stmt `,` ... `}` + /// `switch` expr `if` condition `{` literal or _ `=>` stmt `,` ... `}` Switch( Expr, - Box<(BTreeMap>, StmtBlock)>, + Box<(BTreeMap, StmtBlock)>>, StmtBlock)>, Position, ), /// `while` expr `{` stmt `}` @@ -930,18 +951,11 @@ impl From for StmtBlock { #[inline(always)] fn from(stmt: Stmt) -> Self { match stmt { - Stmt::Block(block, pos) => Self { - statements: block.into(), - pos, - }, - Stmt::Noop(pos) => Self { - statements: Default::default(), - pos, - }, + Stmt::Block(block, pos) => Self(block.into(), pos), + Stmt::Noop(pos) => Self(Default::default(), pos), _ => { let pos = stmt.position(); - let statements = vec![stmt].into(); - Self { statements, pos } + Self(vec![stmt].into(), pos) } } } @@ -1081,28 +1095,26 @@ impl Stmt { Self::Expr(expr) => expr.is_pure(), Self::If(condition, x, _) => { condition.is_pure() - && x.0.statements.iter().all(Stmt::is_pure) - && x.1.statements.iter().all(Stmt::is_pure) + && (x.0).0.iter().all(Stmt::is_pure) + && (x.1).0.iter().all(Stmt::is_pure) } Self::Switch(expr, x, _) => { expr.is_pure() - && x.0 - .values() - .flat_map(|block| block.statements.iter()) - .all(Stmt::is_pure) - && x.1.statements.iter().all(Stmt::is_pure) + && x.0.values().all(|block| { + block.0.as_ref().map(Expr::is_pure).unwrap_or(true) + && (block.1).0.iter().all(Stmt::is_pure) + }) + && (x.1).0.iter().all(Stmt::is_pure) } Self::While(condition, block, _) | Self::Do(block, condition, _, _) => { - condition.is_pure() && block.statements.iter().all(Stmt::is_pure) - } - Self::For(iterable, x, _) => { - iterable.is_pure() && x.1.statements.iter().all(Stmt::is_pure) + condition.is_pure() && block.0.iter().all(Stmt::is_pure) } + Self::For(iterable, x, _) => iterable.is_pure() && (x.1).0.iter().all(Stmt::is_pure), Self::Let(_, _, _, _) | Self::Const(_, _, _, _) | Self::Assignment(_, _) => false, Self::Block(block, _) => block.iter().all(|stmt| stmt.is_pure()), Self::Continue(_) | Self::Break(_) | Self::Return(_, _, _) => false, Self::TryCatch(x, _, _) => { - x.0.statements.iter().all(Stmt::is_pure) && x.2.statements.iter().all(Stmt::is_pure) + (x.0).0.iter().all(Stmt::is_pure) && (x.2).0.iter().all(Stmt::is_pure) } #[cfg(not(feature = "no_module"))] @@ -1168,12 +1180,12 @@ impl Stmt { if !e.walk(path, on_node) { return false; } - for s in &x.0.statements { + for s in &(x.0).0 { if !s.walk(path, on_node) { return false; } } - for s in &x.1.statements { + for s in &(x.1).0 { if !s.walk(path, on_node) { return false; } @@ -1183,12 +1195,17 @@ impl Stmt { if !e.walk(path, on_node) { return false; } - for s in x.0.values().flat_map(|block| block.statements.iter()) { - if !s.walk(path, on_node) { + for b in x.0.values() { + if !b.0.as_ref().map(|e| e.walk(path, on_node)).unwrap_or(true) { return false; } + for s in &(b.1).0 { + if !s.walk(path, on_node) { + return false; + } + } } - for s in &x.1.statements { + for s in &(x.1).0 { if !s.walk(path, on_node) { return false; } @@ -1198,7 +1215,7 @@ impl Stmt { if !e.walk(path, on_node) { return false; } - for s in &s.statements { + for s in &s.0 { if !s.walk(path, on_node) { return false; } @@ -1208,7 +1225,7 @@ impl Stmt { if !e.walk(path, on_node) { return false; } - for s in &x.1.statements { + for s in &(x.1).0 { if !s.walk(path, on_node) { return false; } @@ -1230,12 +1247,12 @@ impl Stmt { } } Self::TryCatch(x, _, _) => { - for s in &x.0.statements { + for s in &(x.0).0 { if !s.walk(path, on_node) { return false; } } - for s in &x.2.statements { + for s in &(x.2).0 { if !s.walk(path, on_node) { return false; } @@ -1283,7 +1300,7 @@ pub struct CustomExpr { /// # Volatile Data Structure /// /// This type is volatile and may change. -#[derive(Debug, Clone, Hash)] +#[derive(Clone, Hash)] pub struct BinaryExpr { /// LHS expression. pub lhs: Expr, @@ -1419,7 +1436,7 @@ impl FnCallHash { /// # Volatile Data Structure /// /// This type is volatile and may change. -#[derive(Debug, Clone, Default, Hash)] +#[derive(Clone, Default, Hash)] pub struct FnCallExpr { /// Pre-calculated hash. pub hash: FnCallHash, @@ -1668,12 +1685,12 @@ impl fmt::Debug for Expr { Self::Variable(i, pos, x) => { f.write_str("Variable(")?; match x.1 { - Some((_, ref namespace)) => write!(f, "{}::", namespace)?, + Some((_, ref namespace)) => write!(f, "{}", namespace)?, _ => (), } write!(f, "{}", x.2)?; match i.map_or_else(|| x.0, |n| NonZeroUsize::new(n.get() as usize)) { - Some(n) => write!(f, " [{}]", n)?, + Some(n) => write!(f, ", {}", n)?, _ => (), } write!(f, ") @ {:?}", pos) @@ -1681,15 +1698,28 @@ impl fmt::Debug for Expr { Self::Property(x) => write!(f, "Property({:?} @ {:?})", x.2.name, x.2.pos), Self::Stmt(x) => { f.write_str("Stmt")?; - f.debug_list().entries(x.statements.iter()).finish()?; - write!(f, " @ {:?}", x.pos) + f.debug_list().entries(x.0.iter()).finish()?; + write!(f, " @ {:?}", x.1) } Self::FnCall(x, pos) => { - f.debug_tuple("FnCall").field(x).finish()?; + let mut ff = f.debug_struct("FnCall"); + if let Some(ref ns) = x.namespace { + ff.field("namespace", ns); + } + ff.field("name", &x.name) + .field("hash", &x.hash) + .field("args", &x.args); + if !x.constant_args.is_empty() { + ff.field("constant_args", &x.constant_args); + } + if x.capture { + ff.field("capture", &x.capture); + } + ff.finish()?; write!(f, " @ {:?}", pos) } Self::Dot(x, pos) | Self::Index(x, pos) | Self::And(x, pos) | Self::Or(x, pos) => { - let op = match self { + let op_name = match self { Self::Dot(_, _) => "Dot", Self::Index(_, _) => "Index", Self::And(_, _) => "And", @@ -1697,7 +1727,7 @@ impl fmt::Debug for Expr { _ => unreachable!(), }; - f.debug_struct(op) + f.debug_struct(op_name) .field("lhs", &x.lhs) .field("rhs", &x.rhs) .finish()?; @@ -1783,7 +1813,7 @@ impl Expr { Self::Array(_, pos) => *pos, Self::Map(_, pos) => *pos, Self::Property(x) => (x.2).pos, - Self::Stmt(x) => x.pos, + Self::Stmt(x) => x.1, Self::Variable(_, pos, _) => *pos, Self::FnCall(_, pos) => *pos, @@ -1816,7 +1846,7 @@ impl Expr { Self::Map(_, pos) => *pos = new_pos, Self::Variable(_, pos, _) => *pos = new_pos, Self::Property(x) => (x.2).pos = new_pos, - Self::Stmt(x) => x.pos = new_pos, + Self::Stmt(x) => x.1 = new_pos, Self::FnCall(_, pos) => *pos = new_pos, Self::And(_, pos) | Self::Or(_, pos) => *pos = new_pos, Self::Unit(pos) => *pos = new_pos, @@ -1840,7 +1870,7 @@ impl Expr { x.lhs.is_pure() && x.rhs.is_pure() } - Self::Stmt(x) => x.statements.iter().all(Stmt::is_pure), + Self::Stmt(x) => x.0.iter().all(Stmt::is_pure), Self::Variable(_, _, _) => true, @@ -1946,7 +1976,7 @@ impl Expr { match self { Self::Stmt(x) => { - for s in &x.statements { + for s in &x.0 { if !s.walk(path, on_node) { return false; } diff --git a/src/engine.rs b/src/engine.rs index 3c2775d2..17274c4e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,6 +1,6 @@ //! Main module defining the script evaluation [`Engine`]. -use crate::ast::{Expr, FnCallExpr, Ident, OpAssignment, ReturnType, Stmt, StmtBlock}; +use crate::ast::{Expr, FnCallExpr, Ident, OpAssignment, ReturnType, Stmt}; use crate::dynamic::{map_std_type_name, AccessMode, Union, Variant}; use crate::fn_native::{ CallableFunction, IteratorFn, OnDebugCallback, OnPrintCallback, OnProgressCallback, @@ -1733,8 +1733,7 @@ impl Engine { // Statement block Expr::Stmt(x) if x.is_empty() => Ok(Dynamic::UNIT), Expr::Stmt(x) => { - let statements = &x.statements; - self.eval_stmt_block(scope, mods, state, lib, this_ptr, statements, true, level) + self.eval_stmt_block(scope, mods, state, lib, this_ptr, x, true, level) } // lhs[idx_expr] @@ -2134,40 +2133,29 @@ impl Engine { } // If statement - Stmt::If(expr, x, _) => { - let ( - StmtBlock { - statements: if_stmt, - .. - }, - StmtBlock { - statements: else_stmt, - .. - }, - ) = x.as_ref(); - self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)? - .as_bool() - .map_err(|err| self.make_type_mismatch_err::(err, expr.position())) - .and_then(|guard_val| { - if guard_val { - if !if_stmt.is_empty() { - self.eval_stmt_block( - scope, mods, state, lib, this_ptr, if_stmt, true, level, - ) - } else { - Ok(Dynamic::UNIT) - } + Stmt::If(expr, x, _) => self + .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? + .as_bool() + .map_err(|err| self.make_type_mismatch_err::(err, expr.position())) + .and_then(|guard_val| { + if guard_val { + if !x.0.is_empty() { + self.eval_stmt_block( + scope, mods, state, lib, this_ptr, &x.0, true, level, + ) } else { - if !else_stmt.is_empty() { - self.eval_stmt_block( - scope, mods, state, lib, this_ptr, else_stmt, true, level, - ) - } else { - Ok(Dynamic::UNIT) - } + Ok(Dynamic::UNIT) } - }) - } + } else { + if !x.1.is_empty() { + self.eval_stmt_block( + scope, mods, state, lib, this_ptr, &x.1, true, level, + ) + } else { + Ok(Dynamic::UNIT) + } + } + }), // Switch statement Stmt::Switch(match_expr, x, _) => { @@ -2180,16 +2168,33 @@ impl Engine { value.hash(hasher); let hash = hasher.finish(); - table.get(&hash).map(|t| { - let statements = &t.statements; + table.get(&hash).and_then(|t| { + if let Some(condition) = &t.0 { + match self + .eval_expr(scope, mods, state, lib, this_ptr, &condition, level) + .and_then(|v| { + v.as_bool().map_err(|err| { + self.make_type_mismatch_err::( + err, + condition.position(), + ) + }) + }) { + Ok(true) => (), + Ok(false) => return None, + Err(err) => return Some(Err(err)), + } + } - if !statements.is_empty() { + let statements = &t.1; + + Some(if !statements.is_empty() { self.eval_stmt_block( scope, mods, state, lib, this_ptr, statements, true, level, ) } else { Ok(Dynamic::UNIT) - } + }) }) } else { // Non-hashable values never match any specific clause @@ -2197,7 +2202,6 @@ impl Engine { } .unwrap_or_else(|| { // Default match clause - let def_stmt = &def_stmt.statements; if !def_stmt.is_empty() { self.eval_stmt_block( scope, mods, state, lib, this_ptr, def_stmt, true, level, @@ -2209,75 +2213,65 @@ impl Engine { } // While loop - Stmt::While(expr, body, _) => { - let body = &body.statements; - loop { - let condition = if !expr.is_unit() { - self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)? - .as_bool() - .map_err(|err| { - self.make_type_mismatch_err::(err, expr.position()) - })? - } else { - true - }; + Stmt::While(expr, body, _) => loop { + let condition = if !expr.is_unit() { + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)? + .as_bool() + .map_err(|err| self.make_type_mismatch_err::(err, expr.position()))? + } else { + true + }; - if !condition { - return Ok(Dynamic::UNIT); - } - if body.is_empty() { - continue; - } + if !condition { + return Ok(Dynamic::UNIT); + } + if body.is_empty() { + continue; + } + match self.eval_stmt_block(scope, mods, state, lib, this_ptr, body, true, level) { + Ok(_) => (), + Err(err) => match *err { + EvalAltResult::LoopBreak(false, _) => (), + EvalAltResult::LoopBreak(true, _) => return Ok(Dynamic::UNIT), + _ => return Err(err), + }, + } + }, + + // Do loop + Stmt::Do(body, expr, is_while, _) => loop { + if !body.is_empty() { match self.eval_stmt_block(scope, mods, state, lib, this_ptr, body, true, level) { Ok(_) => (), Err(err) => match *err { - EvalAltResult::LoopBreak(false, _) => (), + EvalAltResult::LoopBreak(false, _) => continue, EvalAltResult::LoopBreak(true, _) => return Ok(Dynamic::UNIT), _ => return Err(err), }, } } - } - // Do loop - Stmt::Do(body, expr, is_while, _) => { - let body = &body.statements; - - loop { - if !body.is_empty() { - match self - .eval_stmt_block(scope, mods, state, lib, this_ptr, body, true, level) - { - Ok(_) => (), - Err(err) => match *err { - EvalAltResult::LoopBreak(false, _) => continue, - EvalAltResult::LoopBreak(true, _) => return Ok(Dynamic::UNIT), - _ => return Err(err), - }, - } + if self + .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? + .as_bool() + .map_err(|err| self.make_type_mismatch_err::(err, expr.position()))? + { + if !*is_while { + return Ok(Dynamic::UNIT); } - - if self - .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? - .as_bool() - .map_err(|err| self.make_type_mismatch_err::(err, expr.position()))? - { - if !*is_while { - return Ok(Dynamic::UNIT); - } - } else { - if *is_while { - return Ok(Dynamic::UNIT); - } + } else { + if *is_while { + return Ok(Dynamic::UNIT); } } - } + }, // For loop Stmt::For(expr, x, _) => { - let (Ident { name, .. }, StmtBlock { statements, pos }) = x.as_ref(); + let (Ident { name, .. }, statements) = x.as_ref(); + let pos = statements.position(); let iter_obj = self .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? .flatten(); @@ -2326,7 +2320,7 @@ impl Engine { *loop_var = value; } - self.inc_operations(state, *pos)?; + self.inc_operations(state, pos)?; if statements.is_empty() { continue; @@ -2360,20 +2354,10 @@ impl Engine { // Try/Catch statement Stmt::TryCatch(x, _, _) => { - let ( - StmtBlock { - statements: try_body, - .. - }, - err_var, - StmtBlock { - statements: catch_body, - .. - }, - ) = x.as_ref(); + let (try_stmt, err_var, catch_stmt) = x.as_ref(); let result = self - .eval_stmt_block(scope, mods, state, lib, this_ptr, try_body, true, level) + .eval_stmt_block(scope, mods, state, lib, this_ptr, try_stmt, true, level) .map(|_| Dynamic::UNIT); match result { @@ -2433,7 +2417,7 @@ impl Engine { } let result = self.eval_stmt_block( - scope, mods, state, lib, this_ptr, catch_body, true, level, + scope, mods, state, lib, this_ptr, catch_stmt, true, level, ); state.scope_level -= 1; diff --git a/src/fn_call.rs b/src/fn_call.rs index c0d2845e..9f9984e7 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -540,7 +540,7 @@ impl Engine { } // Evaluate the function - let body = &fn_def.body.statements; + let body = &fn_def.body; let result = self .eval_stmt_block(scope, mods, state, unified_lib, this_ptr, body, true, level) diff --git a/src/optimize.rs b/src/optimize.rs index 49a58a1c..2f3111bd 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -1,6 +1,6 @@ //! Module implementing the [`AST`] optimizer. -use crate::ast::{Expr, Stmt, StmtBlock}; +use crate::ast::{Expr, Stmt}; use crate::dynamic::AccessMode; use crate::engine::{KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_PRINT, KEYWORD_TYPE_OF}; use crate::fn_builtin::get_builtin_binary_op_fn; @@ -416,73 +416,123 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut State, preserve_result: bool) { // if false { if_block } else { else_block } -> else_block Stmt::If(Expr::BoolConstant(false, _), x, _) => { state.set_dirty(); - let else_block = mem::take(&mut x.1.statements).into_vec(); + let else_block = mem::take(&mut *x.1).into_vec(); *stmt = match optimize_stmt_block(else_block, state, preserve_result, true, false) { - statements if statements.is_empty() => Stmt::Noop(x.1.pos), - statements => Stmt::Block(statements, x.1.pos), + statements if statements.is_empty() => Stmt::Noop(x.1.position()), + statements => Stmt::Block(statements, x.1.position()), } } // if true { if_block } else { else_block } -> if_block Stmt::If(Expr::BoolConstant(true, _), x, _) => { state.set_dirty(); - let if_block = mem::take(&mut x.0.statements).into_vec(); + let if_block = mem::take(&mut *x.0).into_vec(); *stmt = match optimize_stmt_block(if_block, state, preserve_result, true, false) { - statements if statements.is_empty() => Stmt::Noop(x.0.pos), - statements => Stmt::Block(statements, x.0.pos), + statements if statements.is_empty() => Stmt::Noop(x.0.position()), + statements => Stmt::Block(statements, x.0.position()), } } // if expr { if_block } else { else_block } Stmt::If(condition, x, _) => { optimize_expr(condition, state); - let if_block = mem::take(&mut x.0.statements).into_vec(); - x.0.statements = + let if_block = mem::take(x.0.statements()).into_vec(); + *x.0.statements() = optimize_stmt_block(if_block, state, preserve_result, true, false).into(); - let else_block = mem::take(&mut x.1.statements).into_vec(); - x.1.statements = + let else_block = mem::take(x.1.statements()).into_vec(); + *x.1.statements() = optimize_stmt_block(else_block, state, preserve_result, true, false).into(); } // switch const { ... } - Stmt::Switch(expr, x, pos) if expr.is_constant() => { - let value = expr.get_constant_value().unwrap(); + Stmt::Switch(match_expr, x, pos) if match_expr.is_constant() => { + let value = match_expr.get_constant_value().unwrap(); let hasher = &mut get_hasher(); value.hash(hasher); let hash = hasher.finish(); state.set_dirty(); - let table = &mut x.0; - let (statements, new_pos) = if let Some(block) = table.get_mut(&hash) { - let match_block = mem::take(&mut block.statements).into_vec(); - ( - optimize_stmt_block(match_block, state, true, true, false).into(), - block.pos, - ) - } else { - let def_block = mem::take(&mut x.1.statements).into_vec(); - ( - optimize_stmt_block(def_block, state, true, true, false).into(), - if x.1.pos.is_none() { *pos } else { x.1.pos }, - ) - }; + if let Some(block) = table.get_mut(&hash) { + if let Some(mut condition) = mem::take(&mut block.0) { + // switch const { case if condition => stmt, _ => def } => if condition { stmt } else { def } + optimize_expr(&mut condition, state); - *expr = Expr::Stmt(Box::new(StmtBlock { - statements, - pos: new_pos, - })); + let def_block = mem::take(&mut *x.1).into_vec(); + let def_stmt = optimize_stmt_block(def_block, state, true, true, false); + let def_pos = if x.1.position().is_none() { + *pos + } else { + x.1.position() + }; + + *stmt = Stmt::If( + condition, + Box::new(( + mem::take(&mut block.1), + Stmt::Block(def_stmt, def_pos).into(), + )), + match_expr.position(), + ); + } else { + // Promote the matched case + let new_pos = block.1.position(); + let statements = mem::take(&mut *block.1); + let statements = + optimize_stmt_block(statements.into_vec(), state, true, true, false); + *stmt = Stmt::Block(statements, new_pos); + } + } else { + // Promote the default case + let def_block = mem::take(&mut *x.1).into_vec(); + let def_stmt = optimize_stmt_block(def_block, state, true, true, false); + let def_pos = if x.1.position().is_none() { + *pos + } else { + x.1.position() + }; + *stmt = Stmt::Block(def_stmt, def_pos); + } } // switch - Stmt::Switch(expr, x, _) => { - optimize_expr(expr, state); + Stmt::Switch(match_expr, x, _) => { + optimize_expr(match_expr, state); x.0.values_mut().for_each(|block| { - let match_block = mem::take(&mut block.statements).into_vec(); - block.statements = - optimize_stmt_block(match_block, state, preserve_result, true, false).into() + let condition = if let Some(mut condition) = mem::take(&mut block.0) { + optimize_expr(&mut condition, state); + condition + } else { + Expr::Unit(Position::NONE) + }; + + match condition { + Expr::Unit(_) | Expr::BoolConstant(true, _) => (), + _ => { + block.0 = Some(condition); + + *block.1.statements() = optimize_stmt_block( + mem::take(block.1.statements()).into_vec(), + state, + preserve_result, + true, + false, + ) + .into(); + } + } }); - let def_block = mem::take(&mut x.1.statements).into_vec(); - x.1.statements = - optimize_stmt_block(def_block, state, preserve_result, true, false).into() + + // Remove false cases + while let Some((&key, _)) = x.0.iter().find(|(_, block)| match block.0 { + Some(Expr::BoolConstant(false, _)) => true, + _ => false, + }) { + state.set_dirty(); + x.0.remove(&key); + } + + let def_block = mem::take(x.1.statements()).into_vec(); + *x.1.statements() = + optimize_stmt_block(def_block, state, preserve_result, true, false).into(); } // while false { block } -> Noop @@ -493,12 +543,11 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut State, preserve_result: bool) { // while expr { block } Stmt::While(condition, body, _) => { optimize_expr(condition, state); - - let block = mem::take(&mut body.statements).into_vec(); - body.statements = optimize_stmt_block(block, state, false, true, false).into(); + let block = mem::take(body.statements()).into_vec(); + *body.statements() = optimize_stmt_block(block, state, false, true, false).into(); if body.len() == 1 { - match body.statements[0] { + match body[0] { // while expr { break; } -> { expr; } Stmt::Break(pos) => { // Only a single break statement - turn into running the guard expression once @@ -521,23 +570,24 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut State, preserve_result: bool) { Stmt::Do(body, Expr::BoolConstant(true, _), false, _) | Stmt::Do(body, Expr::BoolConstant(false, _), true, _) => { state.set_dirty(); - let block = mem::take(&mut body.statements).into_vec(); + let block_pos = body.position(); + let block = mem::take(body.statements()).into_vec(); *stmt = Stmt::Block( optimize_stmt_block(block, state, false, true, false), - body.pos, + block_pos, ); } // do { block } while|until expr Stmt::Do(body, condition, _, _) => { optimize_expr(condition, state); - let block = mem::take(&mut body.statements).into_vec(); - body.statements = optimize_stmt_block(block, state, false, true, false).into(); + let block = mem::take(body.statements()).into_vec(); + *body.statements() = optimize_stmt_block(block, state, false, true, false).into(); } // for id in expr { block } Stmt::For(iterable, x, _) => { optimize_expr(iterable, state); - let body = mem::take(&mut x.1.statements).into_vec(); - x.1.statements = optimize_stmt_block(body, state, false, true, false).into(); + let body = mem::take(x.1.statements()).into_vec(); + *x.1.statements() = optimize_stmt_block(body, state, false, true, false).into(); } // let id = expr; Stmt::Let(expr, _, _, _) => optimize_expr(expr, state), @@ -563,31 +613,32 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut State, preserve_result: bool) { } } // try { pure try_block } catch ( var ) { catch_block } -> try_block - Stmt::TryCatch(x, _, _) if x.0.statements.iter().all(Stmt::is_pure) => { + Stmt::TryCatch(x, _, _) if x.0.iter().all(Stmt::is_pure) => { // If try block is pure, there will never be any exceptions state.set_dirty(); - let try_block = mem::take(&mut x.0.statements).into_vec(); + let try_pos = x.0.position(); + let try_block = mem::take(&mut *x.0).into_vec(); *stmt = Stmt::Block( optimize_stmt_block(try_block, state, false, true, false), - x.0.pos, + try_pos, ); } // try { try_block } catch ( var ) { catch_block } Stmt::TryCatch(x, _, _) => { - let try_block = mem::take(&mut x.0.statements).into_vec(); - x.0.statements = optimize_stmt_block(try_block, state, false, true, false).into(); - let catch_block = mem::take(&mut x.2.statements).into_vec(); - x.2.statements = optimize_stmt_block(catch_block, state, false, true, false).into(); + let try_block = mem::take(x.0.statements()).into_vec(); + *x.0.statements() = optimize_stmt_block(try_block, state, false, true, false).into(); + let catch_block = mem::take(x.2.statements()).into_vec(); + *x.2.statements() = optimize_stmt_block(catch_block, state, false, true, false).into(); } // {} - Stmt::Expr(Expr::Stmt(x)) if x.statements.is_empty() => { + Stmt::Expr(Expr::Stmt(x)) if x.is_empty() => { state.set_dirty(); - *stmt = Stmt::Noop(x.pos); + *stmt = Stmt::Noop(x.position()); } // {...}; Stmt::Expr(Expr::Stmt(x)) => { state.set_dirty(); - *stmt = Stmt::Block(mem::take(&mut x.statements).into_vec(), x.pos); + *stmt = mem::take(x.as_mut()).into(); } // expr; Stmt::Expr(expr) => optimize_expr(expr, state), @@ -610,13 +661,13 @@ fn optimize_expr(expr: &mut Expr, state: &mut State) { match expr { // {} - Expr::Stmt(x) if x.statements.is_empty() => { state.set_dirty(); *expr = Expr::Unit(x.pos) } + Expr::Stmt(x) if x.is_empty() => { state.set_dirty(); *expr = Expr::Unit(x.position()) } // { stmt; ... } - do not count promotion as dirty because it gets turned back into an array Expr::Stmt(x) => { - x.statements = optimize_stmt_block(mem::take(&mut x.statements).into_vec(), state, true, true, false).into(); + *x.statements() = optimize_stmt_block(mem::take(x.statements()).into_vec(), state, true, true, false).into(); // { Stmt(Expr) } - promote - match x.statements.as_mut() { + match x.as_mut().as_mut() { [ Stmt::Expr(e) ] => { state.set_dirty(); *expr = mem::take(e); } _ => () } @@ -1034,19 +1085,13 @@ pub fn optimize_into_ast( .map(|fn_def| { let mut fn_def = crate::fn_native::shared_take_or_clone(fn_def); - let pos = fn_def.body.pos; - - let mut body = fn_def.body.statements.into_vec(); - // Optimize the function body let state = &mut State::new(engine, lib2, level); - body = optimize_stmt_block(body, state, true, true, true); + let body = mem::take(fn_def.body.statements()).into_vec(); - fn_def.body = StmtBlock { - statements: body.into(), - pos, - }; + *fn_def.body.statements() = + optimize_stmt_block(body, state, true, true, true).into(); fn_def }) diff --git a/src/parse_error.rs b/src/parse_error.rs index a0a62e00..a7b75cba 100644 --- a/src/parse_error.rs +++ b/src/parse_error.rs @@ -113,8 +113,12 @@ pub enum ParseErrorType { /// /// Never appears under the `no_object` feature. DuplicatedProperty(String), - /// A switch case is duplicated. + /// A `switch` case is duplicated. DuplicatedSwitchCase, + /// The default case of a `switch` statement is not the last. + WrongSwitchDefaultCase, + /// The case condition of a `switch` statement is not appropriate. + WrongSwitchCaseCondition, /// Missing a property name for custom types and maps. /// /// Never appears under the `no_object` feature. @@ -195,6 +199,8 @@ impl ParseErrorType { Self::MalformedCapture(_) => "Invalid capturing", Self::DuplicatedProperty(_) => "Duplicated property in object map literal", Self::DuplicatedSwitchCase => "Duplicated switch case", + Self::WrongSwitchDefaultCase => "Default switch case is not the last", + Self::WrongSwitchCaseCondition => "Default switch case cannot have condition", Self::PropertyExpected => "Expecting name of a property", Self::VariableExpected => "Expecting name of a variable", Self::Reserved(_) => "Invalid use of reserved keyword", diff --git a/src/parser.rs b/src/parser.rs index 18de1c75..d376d7f2 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -808,13 +808,14 @@ fn parse_switch( } } - let mut table = BTreeMap::>::new(); + let mut table = BTreeMap::, StmtBlock)>>::new(); + let mut def_pos = Position::NONE; let mut def_stmt = None; loop { const MISSING_RBRACE: &str = "to end this switch block"; - let expr = match input.peek().unwrap() { + let (expr, condition) = match input.peek().unwrap() { (Token::RightBrace, _) => { eat_token(input, Token::RightBrace); break; @@ -825,12 +826,31 @@ fn parse_switch( .into_err(*pos), ) } - (Token::Underscore, _) if def_stmt.is_none() => { + (Token::Underscore, pos) if def_stmt.is_none() => { + def_pos = *pos; eat_token(input, Token::Underscore); - None + + let (if_clause, if_pos) = match_token(input, Token::If); + + if if_clause { + return Err(PERR::WrongSwitchCaseCondition.into_err(if_pos)); + } + + (None, None) } (Token::Underscore, pos) => return Err(PERR::DuplicatedSwitchCase.into_err(*pos)), - _ => Some(parse_expr(input, state, lib, settings.level_up())?), + _ if def_stmt.is_some() => return Err(PERR::WrongSwitchDefaultCase.into_err(def_pos)), + + _ => { + let case_expr = Some(parse_expr(input, state, lib, settings.level_up())?); + + let condition = if match_token(input, Token::If).0 { + Some(parse_expr(input, state, lib, settings.level_up())?) + } else { + None + }; + (case_expr, condition) + } }; let hash = if let Some(expr) = expr { @@ -868,7 +888,7 @@ fn parse_switch( let need_comma = !stmt.is_self_terminated(); def_stmt = if let Some(hash) = hash { - table.insert(hash, Box::new(stmt.into())); + table.insert(hash, Box::new((condition, stmt.into()))); None } else { Some(stmt.into()) @@ -2875,7 +2895,7 @@ fn make_curry_from_externals( let mut statements: StaticVec<_> = Default::default(); statements.extend(externals.into_iter().map(Stmt::Share)); statements.push(Stmt::Expr(expr)); - Expr::Stmt(Box::new(StmtBlock { statements, pos })) + Expr::Stmt(Box::new(StmtBlock::new(statements, pos))) } /// Parse an anonymous function definition. diff --git a/src/token.rs b/src/token.rs index 34c00034..d12831ca 100644 --- a/src/token.rs +++ b/src/token.rs @@ -866,14 +866,10 @@ pub trait InputStream { /// Returns the parsed string and a boolean indicating whether the string is /// terminated by an interpolation `${`. /// -/// # Volatile API -/// -/// This function is volatile and may change. -/// /// # Returns /// /// |Type |Return Value |`state.is_within_text_terminated_by`| -/// |---------------------------------|----------------------------|------------------------------------| +/// |---------------------------------|:--------------------------:|:----------------------------------:| /// |`"hello"` |`StringConstant("hello")` |`None` | /// |`"hello`_{LF}_ or _{EOF}_ |`LexError` |`None` | /// |`"hello\`_{EOF}_ or _{LF}{EOF}_ |`StringConstant("hello")` |`Some('"')` | @@ -882,6 +878,22 @@ pub trait InputStream { /// |`` `hello ${`` |`InterpolatedString("hello ")`
next token is `{`|`None` | /// |`` } hello` `` |`StringConstant(" hello")` |`None` | /// |`} hello`_{EOF}_ |`StringConstant(" hello")` |``Some('`')`` | +/// +/// This function does not throw a `LexError` for the following conditions: +/// +/// * Unterminated literal string at _{EOF}_ +/// +/// * Unterminated normal string with continuation at _{EOF}_ +/// +/// This is to facilitate using this function to parse a script line-by-line, where the end of the +/// line (i.e. _{EOF}_) is not necessarily the end of the script. +/// +/// Any time a [`StringConstant`][`Token::StringConstant`] is returned with +/// `state.is_within_text_terminated_by` set to `Some(_)` is one of the above conditions. +/// +/// # Volatile API +/// +/// This function is volatile and may change. pub fn parse_string_literal( stream: &mut impl InputStream, state: &mut TokenizeState, @@ -1947,7 +1959,11 @@ impl<'a> Iterator for TokenIterator<'a> { let (token, pos) = match get_next_token(&mut self.stream, &mut self.state, &mut self.pos) { // {EOF} None => return None, - // {EOF} after unterminated string + // {EOF} after unterminated string. + // The only case where `TokenizeState.is_within_text_terminated_by` is set is when + // a verbatim string or a string with continuation encounters {EOF}. + // This is necessary to handle such cases for line-by-line parsing, but for an entire + // script it is a syntax error. Some((Token::StringConstant(_), pos)) if self.state.is_within_text_terminated_by.is_some() => { self.state.is_within_text_terminated_by = None; return Some((Token::LexError(LERR::UnterminatedString), pos)); diff --git a/tests/switch.rs b/tests/switch.rs index 537dfd04..db0d8e17 100644 --- a/tests/switch.rs +++ b/tests/switch.rs @@ -1,4 +1,4 @@ -use rhai::{Engine, EvalAltResult, Scope, INT}; +use rhai::{Engine, EvalAltResult, ParseErrorType, Scope, INT}; #[test] fn test_switch() -> Result<(), Box> { @@ -67,6 +67,93 @@ fn test_switch() -> Result<(), Box> { Ok(()) } +#[test] +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 }") + .expect_err("should error") + .0, + ParseErrorType::WrongSwitchDefaultCase + )); + + Ok(()) +} + +#[test] +fn test_switch_condition() -> 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, + r" + switch x / 2 { + 21 if x > 40 => 1, + 0 if x < 100 => 2, + 1 => 3, + _ => 9 + } + " + )?, + 1 + ); + + assert_eq!( + engine.eval_with_scope::( + &mut scope, + r" + switch x / 2 { + 21 if x < 40 => 1, + 0 if x < 100 => 2, + 1 => 3, + _ => 9 + } + " + )?, + 9 + ); + + assert!(matches!( + *engine + .compile( + r" + 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!(matches!( + *engine + .compile("switch x { 1 => 123, _ if true => 42 }") + .expect_err("should error") + .0, + ParseErrorType::WrongSwitchCaseCondition + )); + + Ok(()) +} + #[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_object"))] mod test_switch_enum {