Add loop expressions.
This commit is contained in:
parent
6af66d3ed3
commit
c14fbdb14d
@ -10,6 +10,7 @@ Bug fixes
|
|||||||
* `Engine::parse_json` now returns an error on unquoted keys to be consistent with JSON specifications.
|
* `Engine::parse_json` now returns an error on unquoted keys to be consistent with JSON specifications.
|
||||||
* `import` statements inside `eval` no longer cause errors in subsequent code.
|
* `import` statements inside `eval` no longer cause errors in subsequent code.
|
||||||
* Functions marked `global` in `import`ed modules with no alias names now work properly.
|
* Functions marked `global` in `import`ed modules with no alias names now work properly.
|
||||||
|
* Incorrect loop optimizations that are too aggressive (e.g. unrolling a `do { ... } until true` with a `break` statement inside) and cause crashes are removed.
|
||||||
|
|
||||||
Speed Improvements
|
Speed Improvements
|
||||||
------------------
|
------------------
|
||||||
@ -19,6 +20,12 @@ Speed Improvements
|
|||||||
New features
|
New features
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
### Loop expressions
|
||||||
|
|
||||||
|
* Loops (such as `loop`, `do`, `while` and `for`) can now act as _expressions_, with the `break` statement returning an optional value.
|
||||||
|
* Normal loops return `()` as the value.
|
||||||
|
* Loop expressions can be enabled/disabled via `Engine::set_allow_loop_expressions`
|
||||||
|
|
||||||
### Stable hashing
|
### Stable hashing
|
||||||
|
|
||||||
* It is now possible to specify a fixed _seed_ for use with the `ahash` hasher, via an environment variable, in order to force stable (i.e. deterministic) hashes for function signatures.
|
* It is now possible to specify a fixed _seed_ for use with the `ahash` hasher, via an environment variable, in order to force stable (i.e. deterministic) hashes for function signatures.
|
||||||
|
@ -12,23 +12,25 @@ bitflags! {
|
|||||||
const IF_EXPR = 0b_0000_0000_0001;
|
const IF_EXPR = 0b_0000_0000_0001;
|
||||||
/// Is `switch` expression allowed?
|
/// Is `switch` expression allowed?
|
||||||
const SWITCH_EXPR = 0b_0000_0000_0010;
|
const SWITCH_EXPR = 0b_0000_0000_0010;
|
||||||
|
/// Are loop expressions allowed?
|
||||||
|
const LOOP_EXPR = 0b_0000_0000_0100;
|
||||||
/// Is statement-expression allowed?
|
/// Is statement-expression allowed?
|
||||||
const STMT_EXPR = 0b_0000_0000_0100;
|
const STMT_EXPR = 0b_0000_0000_1000;
|
||||||
/// Is anonymous function allowed?
|
/// Is anonymous function allowed?
|
||||||
#[cfg(not(feature = "no_function"))]
|
#[cfg(not(feature = "no_function"))]
|
||||||
const ANON_FN = 0b_0000_0000_1000;
|
const ANON_FN = 0b_0000_0001_0000;
|
||||||
/// Is looping allowed?
|
/// Is looping allowed?
|
||||||
const LOOPING = 0b_0000_0001_0000;
|
const LOOPING = 0b_0000_0010_0000;
|
||||||
/// Is variables shadowing allowed?
|
/// Is variables shadowing allowed?
|
||||||
const SHADOW = 0b_0000_0010_0000;
|
const SHADOW = 0b_0000_0100_0000;
|
||||||
/// Strict variables mode?
|
/// Strict variables mode?
|
||||||
const STRICT_VAR = 0b_0000_0100_0000;
|
const STRICT_VAR = 0b_0000_1000_0000;
|
||||||
/// Raise error if an object map property does not exist?
|
/// Raise error if an object map property does not exist?
|
||||||
/// Returns `()` if `false`.
|
/// Returns `()` if `false`.
|
||||||
#[cfg(not(feature = "no_object"))]
|
#[cfg(not(feature = "no_object"))]
|
||||||
const FAIL_ON_INVALID_MAP_PROPERTY = 0b_0000_1000_0000;
|
const FAIL_ON_INVALID_MAP_PROPERTY = 0b_0001_0000_0000;
|
||||||
/// Fast operators mode?
|
/// Fast operators mode?
|
||||||
const FAST_OPS = 0b_0001_0000_0000;
|
const FAST_OPS = 0b_0010_0000_0000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,6 +83,18 @@ impl Engine {
|
|||||||
pub fn set_allow_switch_expression(&mut self, enable: bool) {
|
pub fn set_allow_switch_expression(&mut self, enable: bool) {
|
||||||
self.options.set(LangOptions::SWITCH_EXPR, enable);
|
self.options.set(LangOptions::SWITCH_EXPR, enable);
|
||||||
}
|
}
|
||||||
|
/// Are loop expressions allowed?
|
||||||
|
/// Default is `true`.
|
||||||
|
#[inline(always)]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn allow_loop_expressions(&self) -> bool {
|
||||||
|
self.options.contains(LangOptions::LOOP_EXPR)
|
||||||
|
}
|
||||||
|
/// Set whether loop expressions are allowed.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn set_allow_loop_expressions(&mut self, enable: bool) {
|
||||||
|
self.options.set(LangOptions::LOOP_EXPR, enable);
|
||||||
|
}
|
||||||
/// Is statement-expression allowed?
|
/// Is statement-expression allowed?
|
||||||
/// Default is `true`.
|
/// Default is `true`.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
|
@ -581,14 +581,14 @@ pub enum Stmt {
|
|||||||
TryCatch(Box<TryCatchBlock>, Position),
|
TryCatch(Box<TryCatchBlock>, Position),
|
||||||
/// [expression][Expr]
|
/// [expression][Expr]
|
||||||
Expr(Box<Expr>),
|
Expr(Box<Expr>),
|
||||||
/// `continue`/`break`
|
/// `continue`/`break` expr
|
||||||
///
|
///
|
||||||
/// ### Flags
|
/// ### Flags
|
||||||
///
|
///
|
||||||
/// * [`NONE`][ASTFlags::NONE] = `continue`
|
/// * [`NONE`][ASTFlags::NONE] = `continue`
|
||||||
/// * [`BREAK`][ASTFlags::BREAK] = `break`
|
/// * [`BREAK`][ASTFlags::BREAK] = `break`
|
||||||
BreakLoop(ASTFlags, Position),
|
BreakLoop(Option<Box<Expr>>, ASTFlags, Position),
|
||||||
/// `return`/`throw`
|
/// `return`/`throw` expr
|
||||||
///
|
///
|
||||||
/// ### Flags
|
/// ### Flags
|
||||||
///
|
///
|
||||||
|
@ -493,7 +493,7 @@ impl Engine {
|
|||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(err) => match *err {
|
Err(err) => match *err {
|
||||||
ERR::LoopBreak(false, ..) => (),
|
ERR::LoopBreak(false, ..) => (),
|
||||||
ERR::LoopBreak(true, ..) => break Ok(Dynamic::UNIT),
|
ERR::LoopBreak(true, value, ..) => break Ok(value),
|
||||||
_ => break Err(err),
|
_ => break Err(err),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -524,7 +524,7 @@ impl Engine {
|
|||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(err) => match *err {
|
Err(err) => match *err {
|
||||||
ERR::LoopBreak(false, ..) => (),
|
ERR::LoopBreak(false, ..) => (),
|
||||||
ERR::LoopBreak(true, ..) => break Ok(Dynamic::UNIT),
|
ERR::LoopBreak(true, value, ..) => break Ok(value),
|
||||||
_ => break Err(err),
|
_ => break Err(err),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -547,7 +547,7 @@ impl Engine {
|
|||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(err) => match *err {
|
Err(err) => match *err {
|
||||||
ERR::LoopBreak(false, ..) => continue,
|
ERR::LoopBreak(false, ..) => continue,
|
||||||
ERR::LoopBreak(true, ..) => break Ok(Dynamic::UNIT),
|
ERR::LoopBreak(true, value, ..) => break Ok(value),
|
||||||
_ => break Err(err),
|
_ => break Err(err),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -614,7 +614,7 @@ impl Engine {
|
|||||||
|
|
||||||
let loop_result = func(iter_obj)
|
let loop_result = func(iter_obj)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.try_for_each(|(x, iter_value)| {
|
.try_fold(Dynamic::UNIT, |_, (x, iter_value)| {
|
||||||
// Increment counter
|
// Increment counter
|
||||||
if counter_index < usize::MAX {
|
if counter_index < usize::MAX {
|
||||||
// As the variable increments from 0, this should always work
|
// As the variable increments from 0, this should always work
|
||||||
@ -644,26 +644,26 @@ impl Engine {
|
|||||||
self.track_operation(global, statements.position())?;
|
self.track_operation(global, statements.position())?;
|
||||||
|
|
||||||
if statements.is_empty() {
|
if statements.is_empty() {
|
||||||
return Ok(());
|
return Ok(Dynamic::UNIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.eval_stmt_block(
|
self.eval_stmt_block(
|
||||||
scope, global, caches, lib, this_ptr, statements, true, level,
|
scope, global, caches, lib, this_ptr, statements, true, level,
|
||||||
)
|
)
|
||||||
.map(|_| ())
|
.map(|_| Dynamic::UNIT)
|
||||||
.or_else(|err| match *err {
|
.or_else(|err| match *err {
|
||||||
ERR::LoopBreak(false, ..) => Ok(()),
|
ERR::LoopBreak(false, ..) => Ok(Dynamic::UNIT),
|
||||||
_ => Err(err),
|
_ => Err(err),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.or_else(|err| match *err {
|
.or_else(|err| match *err {
|
||||||
ERR::LoopBreak(true, ..) => Ok(()),
|
ERR::LoopBreak(true, value, ..) => Ok(value),
|
||||||
_ => Err(err),
|
_ => Err(err),
|
||||||
});
|
});
|
||||||
|
|
||||||
scope.rewind(orig_scope_len);
|
scope.rewind(orig_scope_len);
|
||||||
|
|
||||||
loop_result.map(|_| Dynamic::UNIT)
|
loop_result
|
||||||
} else {
|
} else {
|
||||||
Err(ERR::ErrorFor(expr.start_position()).into())
|
Err(ERR::ErrorFor(expr.start_position()).into())
|
||||||
}
|
}
|
||||||
@ -673,8 +673,15 @@ impl Engine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Continue/Break statement
|
// Continue/Break statement
|
||||||
Stmt::BreakLoop(options, pos) => {
|
Stmt::BreakLoop(expr, options, pos) => {
|
||||||
Err(ERR::LoopBreak(options.contains(ASTFlags::BREAK), *pos).into())
|
let is_break = options.contains(ASTFlags::BREAK);
|
||||||
|
|
||||||
|
if let Some(ref expr) = expr {
|
||||||
|
self.eval_expr(scope, global, caches, lib, this_ptr, expr, level)
|
||||||
|
.and_then(|v| ERR::LoopBreak(is_break, v, *pos).into())
|
||||||
|
} else {
|
||||||
|
Err(ERR::LoopBreak(is_break, Dynamic::UNIT, *pos).into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try/Catch statement
|
// Try/Catch statement
|
||||||
|
@ -8,7 +8,7 @@ use crate::engine::{KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_PRINT,
|
|||||||
use crate::eval::{Caches, GlobalRuntimeState};
|
use crate::eval::{Caches, GlobalRuntimeState};
|
||||||
use crate::func::builtin::get_builtin_binary_op_fn;
|
use crate::func::builtin::get_builtin_binary_op_fn;
|
||||||
use crate::func::hashing::get_hasher;
|
use crate::func::hashing::get_hasher;
|
||||||
use crate::tokenizer::{Span, Token};
|
use crate::tokenizer::Token;
|
||||||
use crate::types::dynamic::AccessMode;
|
use crate::types::dynamic::AccessMode;
|
||||||
use crate::{
|
use crate::{
|
||||||
calc_fn_hash, calc_fn_params_hash, combine_hashes, Dynamic, Engine, FnPtr, Identifier,
|
calc_fn_hash, calc_fn_params_hash, combine_hashes, Dynamic, Engine, FnPtr, Identifier,
|
||||||
@ -785,50 +785,6 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
|
|||||||
*condition = Expr::Unit(*pos);
|
*condition = Expr::Unit(*pos);
|
||||||
}
|
}
|
||||||
**body = optimize_stmt_block(mem::take(&mut **body), state, false, true, false);
|
**body = optimize_stmt_block(mem::take(&mut **body), state, false, true, false);
|
||||||
|
|
||||||
if body.len() == 1 {
|
|
||||||
match body[0] {
|
|
||||||
// while expr { break; } -> { expr; }
|
|
||||||
Stmt::BreakLoop(options, pos) if options.contains(ASTFlags::BREAK) => {
|
|
||||||
// Only a single break statement - turn into running the guard expression once
|
|
||||||
state.set_dirty();
|
|
||||||
if condition.is_unit() {
|
|
||||||
*stmt = Stmt::Noop(pos);
|
|
||||||
} else {
|
|
||||||
let mut statements = vec![Stmt::Expr(mem::take(condition).into())];
|
|
||||||
if preserve_result {
|
|
||||||
statements.push(Stmt::Noop(pos));
|
|
||||||
}
|
|
||||||
*stmt = (statements, Span::new(pos, Position::NONE)).into();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// do { block } until true -> { block }
|
|
||||||
Stmt::Do(x, options, ..)
|
|
||||||
if matches!(x.0, Expr::BoolConstant(true, ..))
|
|
||||||
&& options.contains(ASTFlags::NEGATED) =>
|
|
||||||
{
|
|
||||||
state.set_dirty();
|
|
||||||
*stmt = (
|
|
||||||
optimize_stmt_block(mem::take(&mut *x.1), state, false, true, false),
|
|
||||||
x.1.span(),
|
|
||||||
)
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
// do { block } while false -> { block }
|
|
||||||
Stmt::Do(x, options, ..)
|
|
||||||
if matches!(x.0, Expr::BoolConstant(false, ..))
|
|
||||||
&& !options.contains(ASTFlags::NEGATED) =>
|
|
||||||
{
|
|
||||||
state.set_dirty();
|
|
||||||
*stmt = (
|
|
||||||
optimize_stmt_block(mem::take(&mut *x.1), state, false, true, false),
|
|
||||||
x.1.span(),
|
|
||||||
)
|
|
||||||
.into();
|
|
||||||
}
|
}
|
||||||
// do { block } while|until expr
|
// do { block } while|until expr
|
||||||
Stmt::Do(x, ..) => {
|
Stmt::Do(x, ..) => {
|
||||||
@ -916,6 +872,9 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// break expr;
|
||||||
|
Stmt::BreakLoop(Some(ref mut expr), ..) => optimize_expr(expr, state, false),
|
||||||
|
|
||||||
// return expr;
|
// return expr;
|
||||||
Stmt::Return(Some(ref mut expr), ..) => optimize_expr(expr, state, false),
|
Stmt::Return(Some(ref mut expr), ..) => optimize_expr(expr, state, false),
|
||||||
|
|
||||||
|
@ -1380,6 +1380,23 @@ impl Engine {
|
|||||||
self.parse_if(input, state, lib, settings.level_up())?
|
self.parse_if(input, state, lib, settings.level_up())?
|
||||||
.into(),
|
.into(),
|
||||||
)),
|
)),
|
||||||
|
// Loops are allowed to act as expressions
|
||||||
|
Token::While | Token::Loop if settings.options.contains(LangOptions::LOOP_EXPR) => {
|
||||||
|
Expr::Stmt(Box::new(
|
||||||
|
self.parse_while_loop(input, state, lib, settings.level_up())?
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Token::Do if settings.options.contains(LangOptions::LOOP_EXPR) => Expr::Stmt(Box::new(
|
||||||
|
self.parse_do(input, state, lib, settings.level_up())?
|
||||||
|
.into(),
|
||||||
|
)),
|
||||||
|
Token::For if settings.options.contains(LangOptions::LOOP_EXPR) => {
|
||||||
|
Expr::Stmt(Box::new(
|
||||||
|
self.parse_for(input, state, lib, settings.level_up())?
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
// Switch statement is allowed to act as expressions
|
// Switch statement is allowed to act as expressions
|
||||||
Token::Switch if settings.options.contains(LangOptions::SWITCH_EXPR) => {
|
Token::Switch if settings.options.contains(LangOptions::SWITCH_EXPR) => {
|
||||||
Expr::Stmt(Box::new(
|
Expr::Stmt(Box::new(
|
||||||
@ -3411,11 +3428,26 @@ impl Engine {
|
|||||||
|
|
||||||
Token::Continue if self.allow_looping() && settings.is_breakable => {
|
Token::Continue if self.allow_looping() && settings.is_breakable => {
|
||||||
let pos = eat_token(input, Token::Continue);
|
let pos = eat_token(input, Token::Continue);
|
||||||
Ok(Stmt::BreakLoop(ASTFlags::NONE, pos))
|
Ok(Stmt::BreakLoop(None, ASTFlags::NONE, pos))
|
||||||
}
|
}
|
||||||
Token::Break if self.allow_looping() && settings.is_breakable => {
|
Token::Break if self.allow_looping() && settings.is_breakable => {
|
||||||
let pos = eat_token(input, Token::Break);
|
let pos = eat_token(input, Token::Break);
|
||||||
Ok(Stmt::BreakLoop(ASTFlags::BREAK, pos))
|
|
||||||
|
let expr = match input.peek().expect(NEVER_ENDS) {
|
||||||
|
// `break` at <EOF>
|
||||||
|
(Token::EOF, ..) => None,
|
||||||
|
// `break` at end of block
|
||||||
|
(Token::RightBrace, ..) => None,
|
||||||
|
// `break;`
|
||||||
|
(Token::SemiColon, ..) => None,
|
||||||
|
// `break` with expression
|
||||||
|
_ => Some(
|
||||||
|
self.parse_expr(input, state, lib, settings.level_up())?
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Stmt::BreakLoop(expr, ASTFlags::BREAK, pos))
|
||||||
}
|
}
|
||||||
Token::Continue | Token::Break if self.allow_looping() => {
|
Token::Continue | Token::Break if self.allow_looping() => {
|
||||||
Err(PERR::LoopBreak.into_err(token_pos))
|
Err(PERR::LoopBreak.into_err(token_pos))
|
||||||
@ -3840,7 +3872,7 @@ impl Engine {
|
|||||||
let mut functions = StraightHashMap::default();
|
let mut functions = StraightHashMap::default();
|
||||||
|
|
||||||
let mut options = self.options;
|
let mut options = self.options;
|
||||||
options.remove(LangOptions::STMT_EXPR);
|
options.remove(LangOptions::STMT_EXPR | LangOptions::LOOP_EXPR);
|
||||||
#[cfg(not(feature = "no_function"))]
|
#[cfg(not(feature = "no_function"))]
|
||||||
options.remove(LangOptions::ANON_FN);
|
options.remove(LangOptions::ANON_FN);
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ pub enum EvalAltResult {
|
|||||||
/// Breaking out of loops - not an error if within a loop.
|
/// Breaking out of loops - not an error if within a loop.
|
||||||
/// The wrapped value, if true, means breaking clean out of the loop (i.e. a `break` statement).
|
/// The wrapped value, if true, means breaking clean out of the loop (i.e. a `break` statement).
|
||||||
/// The wrapped value, if false, means breaking the current context (i.e. a `continue` statement).
|
/// The wrapped value, if false, means breaking the current context (i.e. a `continue` statement).
|
||||||
LoopBreak(bool, Position),
|
LoopBreak(bool, Dynamic, Position),
|
||||||
/// Not an error: Value returned from a script via the `return` keyword.
|
/// Not an error: Value returned from a script via the `return` keyword.
|
||||||
/// Wrapped value is the result value.
|
/// Wrapped value is the result value.
|
||||||
Return(Dynamic, Position),
|
Return(Dynamic, Position),
|
||||||
|
@ -55,10 +55,13 @@ fn test_expressions() -> Result<(), Box<EvalAltResult>> {
|
|||||||
)
|
)
|
||||||
.is_err());
|
.is_err());
|
||||||
|
|
||||||
assert!(engine.eval_expression::<()>("40 + 2;").is_err());
|
assert!(engine.compile_expression("40 + 2;").is_err());
|
||||||
assert!(engine.eval_expression::<()>("40 + { 2 }").is_err());
|
assert!(engine.compile_expression("40 + { 2 }").is_err());
|
||||||
assert!(engine.eval_expression::<()>("x = 42").is_err());
|
assert!(engine.compile_expression("x = 42").is_err());
|
||||||
assert!(engine.compile_expression("let x = 42").is_err());
|
assert!(engine.compile_expression("let x = 42").is_err());
|
||||||
|
assert!(engine
|
||||||
|
.compile_expression("do { break 42; } while true")
|
||||||
|
.is_err());
|
||||||
|
|
||||||
engine.compile("40 + { let x = 2; x }")?;
|
engine.compile("40 + { let x = 2; x }")?;
|
||||||
|
|
||||||
|
16
tests/for.rs
16
tests/for.rs
@ -231,6 +231,22 @@ fn test_for_loop() -> Result<(), Box<EvalAltResult>> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
engine.eval::<INT>(
|
||||||
|
r#"
|
||||||
|
let a = [123, 999, 42, 0, true, "hello", "world!", 987.6543];
|
||||||
|
|
||||||
|
for (item, count) in a {
|
||||||
|
switch item.type_of() {
|
||||||
|
"i64" if item.is_even => break count,
|
||||||
|
"f64" if item.to_int().is_even => break count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
)?,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,22 @@ fn test_while() -> Result<(), Box<EvalAltResult>> {
|
|||||||
6
|
6
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
engine.eval::<INT>(
|
||||||
|
"
|
||||||
|
let x = 0;
|
||||||
|
|
||||||
|
while x < 10 {
|
||||||
|
x += 1;
|
||||||
|
if x > 5 { break x * 2; }
|
||||||
|
if x > 3 { continue; }
|
||||||
|
x += 3;
|
||||||
|
}
|
||||||
|
",
|
||||||
|
)?,
|
||||||
|
12
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +62,25 @@ fn test_do() -> Result<(), Box<EvalAltResult>> {
|
|||||||
)?,
|
)?,
|
||||||
6
|
6
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
engine.eval::<INT>(
|
||||||
|
"
|
||||||
|
let x = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
x += 1;
|
||||||
|
if x > 5 { break x * 2; }
|
||||||
|
if x > 3 { continue; }
|
||||||
|
x += 3;
|
||||||
|
} while x < 10;
|
||||||
|
",
|
||||||
|
)?,
|
||||||
|
12
|
||||||
|
);
|
||||||
|
|
||||||
|
engine.run("do {} while false")?;
|
||||||
|
|
||||||
|
assert_eq!(engine.eval::<INT>("do { break 42; } while false")?, 42);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user