diff --git a/RELEASES.md b/RELEASES.md index b4f1a290..4d5f3028 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -14,11 +14,13 @@ Breaking changes ---------------- * `Module::set_fn`, `Module::set_raw_fn` and `Module::set_fn_XXX_mut` all take an additional parameter of `FnNamespace`. +* `unless` is now a reserved keyword. New features ------------ * `switch` statement. +* `do ... while` and `do ... until` statement. * `Engine::register_module` to register a module as a sub-module in the global namespace. * `set_exported_global_fn!` macro to register a plugin function and expose it to the global namespace. * `Module::set_fn_XXX_mut` can expose a module function to the global namespace. This is convenient when registering an API for a custom type. diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 8bdce36f..73332147 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -79,6 +79,7 @@ The Rhai Scripting Language 9. [If Statement](language/if.md) 10. [Switch Expression](language/switch.md) 11. [While Loop](language/while.md) + 11. [Do Loop](language/do.md) 12. [Loop Statement](language/loop.md) 13. [For Loop](language/for.md) 14. [Return Values](language/return.md) diff --git a/doc/src/appendix/keywords.md b/doc/src/appendix/keywords.md index ad8d3e26..0e542dcf 100644 --- a/doc/src/appendix/keywords.md +++ b/doc/src/appendix/keywords.md @@ -14,7 +14,9 @@ Keywords List | `if` | if statement | | no | | | `else` | else block of if statement | | no | | | `switch` | matching | | no | | -| `while` | while loop | | no | | +| `do` | looping | | no | | +| `while` | 1) while loop
2) condition for do loop | | no | | +| `until` | do loop | | no | | | `loop` | infinite loop | | no | | | `for` | for loop | | no | | | `in` | 1) containment test
2) part of for loop | | no | | @@ -48,11 +50,11 @@ Reserved Keywords | `var` | variable declaration | | `static` | variable declaration | | `shared` | share value | -| `do` | looping | | `each` | looping | | `then` | control flow | | `goto` | control flow | | `exit` | control flow | +| `unless` | control flow | | `match` | matching | | `case` | matching | | `public` | function/field access | diff --git a/doc/src/language/do.md b/doc/src/language/do.md new file mode 100644 index 00000000..c1129944 --- /dev/null +++ b/doc/src/language/do.md @@ -0,0 +1,28 @@ +`do` Loop +========= + +{{#include ../links.md}} + +`do` loops have two opposite variants: `do` ... `while` and `do` ... `until`. + +Like the `while` loop, `continue` can be used to skip to the next iteration, by-passing all following statements; +`break` can be used to break out of the loop unconditionally. + +```rust +let x = 10; + +do { + x -= 1; + if x < 6 { continue; } // skip to the next iteration + print(x); + if x == 5 { break; } // break out of do loop +} while x > 0; + + +do { + x -= 1; + if x < 6 { continue; } // skip to the next iteration + print(x); + if x == 5 { break; } // break out of do loop +} until x == 0; +``` diff --git a/src/ast.rs b/src/ast.rs index c675ce67..5e71db1e 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -601,8 +601,8 @@ pub enum Stmt { ), /// `while` expr `{` stmt `}` While(Expr, Box, Position), - /// `loop` `{` stmt `}` - Loop(Box, Position), + /// `do` `{` stmt `}` `while`|`until` expr + Do(Box, Expr, bool, Position), /// `for` id `in` expr `{` stmt `}` For(Expr, Box<(String, Stmt)>, Position), /// \[`export`\] `let` id `=` expr @@ -660,7 +660,7 @@ impl Stmt { | Self::If(_, _, pos) | Self::Switch(_, _, pos) | Self::While(_, _, pos) - | Self::Loop(_, pos) + | Self::Do(_, _, _, pos) | Self::For(_, _, pos) | Self::Return((_, pos), _, _) | Self::Let(_, _, _, pos) @@ -689,7 +689,7 @@ impl Stmt { | Self::If(_, _, pos) | Self::Switch(_, _, pos) | Self::While(_, _, pos) - | Self::Loop(_, pos) + | Self::Do(_, _, _, pos) | Self::For(_, _, pos) | Self::Return((_, pos), _, _) | Self::Let(_, _, _, pos) @@ -717,7 +717,6 @@ impl Stmt { Self::If(_, _, _) | Self::Switch(_, _, _) | Self::While(_, _, _) - | Self::Loop(_, _) | Self::For(_, _, _) | Self::Block(_, _) | Self::TryCatch(_, _, _) => true, @@ -729,6 +728,7 @@ impl Stmt { | Self::Const(_, _, _, _) | Self::Assignment(_, _) | Self::Expr(_) + | Self::Do(_, _, _, _) | Self::Continue(_) | Self::Break(_) | Self::Return(_, _, _) => false, @@ -737,7 +737,7 @@ impl Stmt { Self::Import(_, _, _) | Self::Export(_, _) => false, #[cfg(not(feature = "no_closure"))] - Self::Share(_) => false, + Self::Share(_) => unreachable!(), } } /// Is this statement _pure_? @@ -755,8 +755,9 @@ impl Stmt { && x.0.values().all(Stmt::is_pure) && x.1.as_ref().map(Stmt::is_pure).unwrap_or(true) } - Self::While(condition, block, _) => condition.is_pure() && block.is_pure(), - Self::Loop(block, _) => block.is_pure(), + Self::While(condition, block, _) | Self::Do(block, condition, _, _) => { + condition.is_pure() && block.is_pure() + } Self::For(iterable, x, _) => iterable.is_pure() && x.1.is_pure(), Self::Let(_, _, _, _) | Self::Const(_, _, _, _) | Self::Assignment(_, _) => false, Self::Block(block, _) => block.iter().all(|stmt| stmt.is_pure()), diff --git a/src/engine.rs b/src/engine.rs index d15ff852..de065023 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1028,7 +1028,7 @@ impl Engine { })?; } - Ok(Default::default()) + Ok((Dynamic::UNIT, true)) } // xxx[rhs] _ => { @@ -1193,7 +1193,7 @@ impl Engine { |err| match *err { // If there is no setter, no need to feed it back because the property is read-only EvalAltResult::ErrorDotExpr(_, _) => { - Ok(Default::default()) + Ok((Dynamic::UNIT, false)) } _ => Err(err.fill_position(*x_pos)), }, @@ -1902,7 +1902,7 @@ impl Engine { let result = match stmt { // No-op - Stmt::Noop(_) => Ok(Default::default()), + Stmt::Noop(_) => Ok(Dynamic::UNIT), // Expression as statement Stmt::Expr(expr) => self.eval_expr(scope, mods, state, lib, this_ptr, expr, level), @@ -1935,7 +1935,7 @@ impl Engine { } else { *lhs_ptr.as_mut() = rhs_val; } - Ok(Default::default()) + Ok(Dynamic::UNIT) } // Op-assignment - in order of precedence: ScopeEntryType::Normal => { @@ -2003,7 +2003,7 @@ impl Engine { } } } - Ok(Default::default()) + Ok(Dynamic::UNIT) } } } @@ -2045,7 +2045,7 @@ impl Engine { self.eval_dot_index_chain( scope, mods, state, lib, this_ptr, lhs_expr, level, _new_val, )?; - Ok(Default::default()) + Ok(Dynamic::UNIT) } // dot_lhs.dot_rhs op= rhs #[cfg(not(feature = "no_object"))] @@ -2053,7 +2053,7 @@ impl Engine { self.eval_dot_index_chain( scope, mods, state, lib, this_ptr, lhs_expr, level, _new_val, )?; - Ok(Default::default()) + Ok(Dynamic::UNIT) } // Non-lvalue expression (should be caught during parsing) _ => unreachable!(), @@ -2077,7 +2077,7 @@ impl Engine { } else if let Some(stmt) = else_block { self.eval_stmt(scope, mods, state, lib, this_ptr, stmt, level) } else { - Ok(Default::default()) + Ok(Dynamic::UNIT) } }) } @@ -2115,28 +2115,40 @@ impl Engine { Ok(_) => (), Err(err) => match *err { EvalAltResult::LoopBreak(false, _) => (), - EvalAltResult::LoopBreak(true, _) => return Ok(Default::default()), + EvalAltResult::LoopBreak(true, _) => return Ok(Dynamic::UNIT), _ => return Err(err), }, } } - Ok(false) => return Ok(Default::default()), + Ok(false) => return Ok(Dynamic::UNIT), Err(err) => { return Err(self.make_type_mismatch_err::(err, expr.position())) } } }, - // Loop statement - Stmt::Loop(block, _) => loop { - match self.eval_stmt(scope, mods, state, lib, this_ptr, block, level) { + // Do loop + Stmt::Do(body, expr, is_while, _) => loop { + match self.eval_stmt(scope, mods, state, lib, this_ptr, body, level) { Ok(_) => (), Err(err) => match *err { - EvalAltResult::LoopBreak(false, _) => (), - EvalAltResult::LoopBreak(true, _) => return Ok(Default::default()), + EvalAltResult::LoopBreak(false, _) => continue, + EvalAltResult::LoopBreak(true, _) => return Ok(Dynamic::UNIT), _ => return Err(err), }, } + + match self + .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? + .as_bool() + { + Ok(true) if !*is_while => return Ok(Dynamic::UNIT), + Ok(false) if *is_while => return Ok(Dynamic::UNIT), + Ok(_) => (), + Err(err) => { + return Err(self.make_type_mismatch_err::(err, expr.position())) + } + } }, // For loop @@ -2187,7 +2199,7 @@ impl Engine { state.scope_level -= 1; scope.rewind(scope.len() - 1); - Ok(Default::default()) + Ok(Dynamic::UNIT) } else { EvalAltResult::ErrorFor(expr.position()).into() } @@ -2312,7 +2324,7 @@ impl Engine { if let Some(alias) = _alias { scope.add_entry_alias(scope.len() - 1, alias); } - Ok(Default::default()) + Ok(Dynamic::UNIT) } // Import statement @@ -2344,7 +2356,7 @@ impl Engine { state.modules += 1; - Ok(Default::default()) + Ok(Dynamic::UNIT) } else { Err( EvalAltResult::ErrorModuleNotFound(path.to_string(), expr.position()) @@ -2369,7 +2381,7 @@ impl Engine { .into(); } } - Ok(Default::default()) + Ok(Dynamic::UNIT) } // Share statement @@ -2386,7 +2398,7 @@ impl Engine { } _ => (), } - Ok(Default::default()) + Ok(Dynamic::UNIT) } }; diff --git a/src/optimize.rs b/src/optimize.rs index 75c2f5e2..51fe2d2a 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -271,7 +271,7 @@ fn optimize_stmt_block( fn optimize_stmt(stmt: &mut Stmt, state: &mut State, preserve_result: bool) { match stmt { // expr op= expr - Stmt::Assignment(ref mut x, _) => match x.0 { + Stmt::Assignment(x, _) => match x.0 { Expr::Variable(_) => optimize_expr(&mut x.2, state), _ => { optimize_expr(&mut x.0, state); @@ -290,7 +290,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut State, preserve_result: bool) { optimize_stmt(stmt, state, true); } // if expr { Noop } - Stmt::If(ref mut condition, x, _) if x.1.is_none() && matches!(x.0, Stmt::Noop(_)) => { + Stmt::If(condition, x, _) if x.1.is_none() && matches!(x.0, Stmt::Noop(_)) => { state.set_dirty(); let pos = condition.position(); @@ -309,7 +309,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut State, preserve_result: bool) { }; } // if expr { if_block } - Stmt::If(ref mut condition, ref mut x, _) if x.1.is_none() => { + Stmt::If(condition, x, _) if x.1.is_none() => { optimize_expr(condition, state); optimize_stmt(&mut x.0, state, true); } @@ -324,7 +324,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut State, preserve_result: bool) { optimize_stmt(stmt, state, true); } // if expr { if_block } else { else_block } - Stmt::If(ref mut condition, ref mut x, _) => { + Stmt::If(condition, x, _) => { optimize_expr(condition, state); optimize_stmt(&mut x.0, state, true); if let Some(else_block) = x.1.as_mut() { @@ -377,11 +377,6 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut State, preserve_result: bool) { state.set_dirty(); *stmt = Stmt::Noop(*pos) } - // while true { block } -> loop { block } - Stmt::While(Expr::BoolConstant(true, _), block, pos) => { - optimize_stmt(block, state, false); - *stmt = Stmt::Loop(Box::new(mem::take(block)), *pos) - } // while expr { block } Stmt::While(condition, block, _) => { optimize_stmt(block, state, false); @@ -402,32 +397,30 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut State, preserve_result: bool) { _ => (), } } - // loop { block } - Stmt::Loop(block, _) => { - optimize_stmt(block, state, false); - - match **block { - // loop { break; } -> Noop - Stmt::Break(pos) => { - // Only a single break statement - state.set_dirty(); - *stmt = Stmt::Noop(pos) - } - _ => (), - } + // do { block } while false | do { block } until true -> { block } + Stmt::Do(block, Expr::BoolConstant(true, _), false, _) + | Stmt::Do(block, Expr::BoolConstant(false, _), true, _) => { + state.set_dirty(); + optimize_stmt(block.as_mut(), state, false); + *stmt = mem::take(block.as_mut()); + } + // do { block } while|until expr + Stmt::Do(block, condition, _, _) => { + optimize_stmt(block.as_mut(), state, false); + optimize_expr(condition, state); } // for id in expr { block } - Stmt::For(ref mut iterable, ref mut x, _) => { + Stmt::For(iterable, x, _) => { optimize_expr(iterable, state); optimize_stmt(&mut x.1, state, false); } // let id = expr; - Stmt::Let(_, Some(ref mut expr), _, _) => optimize_expr(expr, state), + Stmt::Let(_, Some(expr), _, _) => optimize_expr(expr, state), // let id; Stmt::Let(_, None, _, _) => (), // import expr as var; #[cfg(not(feature = "no_module"))] - Stmt::Import(ref mut expr, _, _) => optimize_expr(expr, state), + Stmt::Import(expr, _, _) => optimize_expr(expr, state), // { block } Stmt::Block(statements, pos) => { *stmt = optimize_stmt_block(mem::take(statements), *pos, state, preserve_result, true); @@ -446,7 +439,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut State, preserve_result: bool) { *stmt = Stmt::Block(statements, pos); } // try { block } catch ( var ) { block } - Stmt::TryCatch(ref mut x, _, _) => { + Stmt::TryCatch(x, _, _) => { optimize_stmt(&mut x.0, state, false); optimize_stmt(&mut x.2, state, false); } @@ -461,7 +454,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut State, preserve_result: bool) { *stmt = Stmt::Block(mem::take(x).into_vec(), *pos); } // expr; - Stmt::Expr(ref mut expr) => optimize_expr(expr, state), + Stmt::Expr(expr) => optimize_expr(expr, state), // return expr; Stmt::Return(_, Some(ref mut expr), _) => optimize_expr(expr, state), diff --git a/src/parser.rs b/src/parser.rs index 9e59f418..aea543c8 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1970,49 +1970,68 @@ fn parse_if( } /// Parse a while loop. -fn parse_while( +fn parse_while_loop( input: &mut TokenStream, state: &mut ParseState, lib: &mut FunctionsLib, mut settings: ParseSettings, ) -> Result { - // while ... - let token_pos = eat_token(input, Token::While); - settings.pos = token_pos; - #[cfg(not(feature = "unchecked"))] settings.ensure_level_within_max_limit(state.max_expr_depth)?; - // while guard { body } - ensure_not_statement_expr(input, "a boolean")?; - let guard = parse_expr(input, state, lib, settings.level_up())?; - ensure_not_assignment(input)?; + // while|loops ... + let (guard, token_pos) = match input.next().unwrap() { + (Token::While, pos) => { + ensure_not_statement_expr(input, "a boolean")?; + (parse_expr(input, state, lib, settings.level_up())?, pos) + } + (Token::Loop, pos) => (Expr::BoolConstant(true, pos), pos), + _ => unreachable!(), + }; + settings.pos = token_pos; + ensure_not_assignment(input)?; settings.is_breakable = true; let body = Box::new(parse_block(input, state, lib, settings.level_up())?); Ok(Stmt::While(guard, body, token_pos)) } -/// Parse a loop statement. -fn parse_loop( +/// Parse a do loop. +fn parse_do( input: &mut TokenStream, state: &mut ParseState, lib: &mut FunctionsLib, mut settings: ParseSettings, ) -> Result { - // loop ... - let token_pos = eat_token(input, Token::Loop); + // do ... + let token_pos = eat_token(input, Token::Do); settings.pos = token_pos; #[cfg(not(feature = "unchecked"))] settings.ensure_level_within_max_limit(state.max_expr_depth)?; - // loop { body } + // do { body } [while|until] guard settings.is_breakable = true; let body = Box::new(parse_block(input, state, lib, settings.level_up())?); - Ok(Stmt::Loop(body, token_pos)) + let is_while = match input.next().unwrap() { + (Token::While, _) => true, + (Token::Until, _) => false, + (_, pos) => { + return Err( + PERR::MissingToken(Token::While.into(), "for the do statement".into()) + .into_err(pos), + ) + } + }; + + ensure_not_statement_expr(input, "a boolean")?; + settings.is_breakable = false; + let guard = parse_expr(input, state, lib, settings.level_up())?; + ensure_not_assignment(input)?; + + Ok(Stmt::Do(body, guard, is_while, token_pos)) } /// Parse a for loop. @@ -2413,8 +2432,10 @@ fn parse_stmt( } Token::If => parse_if(input, state, lib, settings.level_up()).map(Some), - Token::While => parse_while(input, state, lib, settings.level_up()).map(Some), - Token::Loop => parse_loop(input, state, lib, settings.level_up()).map(Some), + Token::While | Token::Loop => { + parse_while_loop(input, state, lib, settings.level_up()).map(Some) + } + Token::Do => parse_do(input, state, lib, settings.level_up()).map(Some), Token::For => parse_for(input, state, lib, settings.level_up()).map(Some), Token::Continue if settings.is_breakable => { diff --git a/src/token.rs b/src/token.rs index 878025b8..c36a5651 100644 --- a/src/token.rs +++ b/src/token.rs @@ -232,8 +232,12 @@ pub enum Token { Else, /// `switch` Switch, + /// `do` + Do, /// `while` While, + /// `until` + Until, /// `loop` Loop, /// `for` @@ -380,7 +384,9 @@ impl Token { If => "if", Else => "else", Switch => "switch", + Do => "do", While => "while", + Until => "until", Loop => "loop", For => "for", In => "in", @@ -467,7 +473,9 @@ impl Token { "if" => If, "else" => Else, "switch" => Switch, + "do" => Do, "while" => While, + "until" => Until, "loop" => Loop, "for" => For, "in" => In, @@ -524,8 +532,8 @@ impl Token { "import" | "export" | "as" => Reserved(syntax.into()), "===" | "!==" | "->" | "<-" | ":=" | "::<" | "(*" | "*)" | "#" | "public" | "new" - | "use" | "module" | "package" | "var" | "static" | "shared" | "with" | "do" - | "each" | "then" | "goto" | "exit" | "match" | "case" | "default" | "void" + | "use" | "module" | "package" | "var" | "static" | "shared" | "with" | "each" + | "then" | "goto" | "unless" | "exit" | "match" | "case" | "default" | "void" | "null" | "nil" | "spawn" | "thread" | "go" | "sync" | "async" | "await" | "yield" => { Reserved(syntax.into()) } @@ -586,7 +594,9 @@ impl Token { Ampersand | And | If | + Do | While | + Until | PlusAssign | MinusAssign | MultiplyAssign | @@ -690,8 +700,8 @@ impl Token { #[cfg(not(feature = "no_module"))] Import | Export | As => true, - True | False | Let | Const | If | Else | While | Loop | For | In | Continue | Break - | Return | Throw | Try | Catch => true, + True | False | Let | Const | If | Else | Do | While | Until | Loop | For | In + | Continue | Break | Return | Throw | Try | Catch => true, _ => false, } diff --git a/tests/while_loop.rs b/tests/while_loop.rs index bbcd091b..f9b74846 100644 --- a/tests/while_loop.rs +++ b/tests/while_loop.rs @@ -24,3 +24,28 @@ fn test_while() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_do() -> Result<(), Box> { + let engine = Engine::new(); + + assert_eq!( + engine.eval::( + r" + let x = 0; + + do { + x += 1; + if x > 5 { break; } + if x > 3 { continue; } + x += 3; + } while x < 10; + + x + ", + )?, + 6 + ); + + Ok(()) +}