From 9f80bf03c4aab2a4fa341d94577b650b77106ec9 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 3 Mar 2020 18:15:20 +0800 Subject: [PATCH] Add throw. --- README.md | 34 ++++++++++++++++++++++++++++++++++ src/engine.rs | 32 ++++++++++++++++++++++++++++---- src/parser.rs | 29 ++++++++++++++++++++++------- tests/throw.rs | 18 ++++++++++++++++++ 4 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 tests/throw.rs diff --git a/README.md b/README.md index 976ac0ae..f1c4aa93 100644 --- a/README.md +++ b/README.md @@ -563,6 +563,40 @@ fn do_addition(x) { } ``` +## Return + +```rust +return; + +return 123 + 456; +``` + +## Errors and Exceptions + +```rust +if error != "" { + throw error; // `throw` takes a string to form the exception text +} + +throw; // no exception text +``` + +All of `Engine`'s evaluation/consuming methods return `Result` with `EvalAltResult` holding error information. + +Exceptions thrown via `throw` in the script can be captured by matching `Err(EvalAltResult::ErrorRuntime(reason, position))` with the exception text captured by the `reason` parameter. + +```rust +let result = engine.eval::(&mut scope, r#" + let x = 42; + + if x > 0 { + throw x + " is too large!"; + } +"#); + +println!(result); // prints "Runtime error: 42 is too large! (line 5, position 15)" +``` + ## Arrays You can create arrays of values, and then access them with numeric indices. diff --git a/src/engine.rs b/src/engine.rs index bccd33a7..8388f2e5 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -35,6 +35,7 @@ pub enum EvalAltResult { ErrorCantOpenScriptFile(String, std::io::Error), ErrorDotExpr(Position), ErrorArithmetic(String, Position), + ErrorRuntime(String, Position), LoopBreak, Return(Dynamic, Position), } @@ -70,6 +71,7 @@ impl Error for EvalAltResult { Self::ErrorCantOpenScriptFile(_, _) => "Cannot open script file", Self::ErrorDotExpr(_) => "Malformed dot expression", Self::ErrorArithmetic(_, _) => "Arithmetic error", + Self::ErrorRuntime(_, _) => "Runtime error", Self::LoopBreak => "[Not Error] Breaks out of loop", Self::Return(_, _) => "[Not Error] Function returns value", } @@ -95,6 +97,8 @@ impl std::fmt::Display for EvalAltResult { Self::ErrorMismatchOutputType(s, pos) => write!(f, "{}: {} ({})", desc, s, pos), Self::ErrorDotExpr(pos) => write!(f, "{} ({})", desc, pos), Self::ErrorArithmetic(s, pos) => write!(f, "{}: {} ({})", desc, s, pos), + Self::ErrorRuntime(s, pos) if s.is_empty() => write!(f, "{} ({})", desc, pos), + Self::ErrorRuntime(s, pos) => write!(f, "{}: {} ({})", desc, s, pos), Self::LoopBreak => write!(f, "{}", desc), Self::Return(_, pos) => write!(f, "{} ({})", desc, pos), Self::ErrorCantOpenScriptFile(filename, err) => { @@ -879,11 +883,31 @@ impl Engine { Stmt::Break(_) => Err(EvalAltResult::LoopBreak), - Stmt::Return(pos) => Err(EvalAltResult::Return(().into_dynamic(), *pos)), + // Empty return + Stmt::ReturnWithVal(None, true, pos) => { + Err(EvalAltResult::Return(().into_dynamic(), *pos)) + } - Stmt::ReturnWithVal(a, pos) => { - let result = self.eval_expr(scope, a)?; - Err(EvalAltResult::Return(result, *pos)) + // Return value + Stmt::ReturnWithVal(Some(a), true, pos) => { + Err(EvalAltResult::Return(self.eval_expr(scope, a)?, *pos)) + } + + // Empty throw + Stmt::ReturnWithVal(None, false, pos) => { + Err(EvalAltResult::ErrorRuntime("".into(), *pos)) + } + + // Throw value + Stmt::ReturnWithVal(Some(a), false, pos) => { + let val = self.eval_expr(scope, a)?; + Err(EvalAltResult::ErrorRuntime( + (val.downcast_ref() as Option<&String>) + .map(|s| s.as_ref()) + .unwrap_or("") + .to_string(), + *pos, + )) } Stmt::Let(name, init, _) => { diff --git a/src/parser.rs b/src/parser.rs index d0f0c580..228ecc70 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -222,8 +222,7 @@ pub enum Stmt { Block(Vec), Expr(Box), Break(Position), - Return(Position), - ReturnWithVal(Box, Position), + ReturnWithVal(Option>, bool, Position), } #[derive(Debug, Clone)] @@ -310,6 +309,7 @@ pub enum Token { Fn, Break, Return, + Throw, PlusAssign, MinusAssign, MultiplyAssign, @@ -382,6 +382,7 @@ impl Token { Fn => "fn", Break => "break", Return => "return", + Throw => "throw", PlusAssign => "+=", MinusAssign => "-=", MultiplyAssign => "*=", @@ -456,6 +457,7 @@ impl Token { Modulo | ModuloAssign | Return | + Throw | PowerOf | In | PowerOfAssign => true, @@ -498,7 +500,7 @@ impl Token { use self::Token::*; match *self { - UnaryPlus | UnaryMinus | Equals | Bang | Return => true, + UnaryPlus | UnaryMinus | Equals | Bang | Return | Throw => true, _ => false, } } @@ -816,6 +818,7 @@ impl<'a> TokenIterator<'a> { "loop" => Token::Loop, "break" => Token::Break, "return" => Token::Return, + "throw" => Token::Throw, "fn" => Token::Fn, "for" => Token::For, "in" => Token::In, @@ -1743,6 +1746,7 @@ fn parse_block<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { + Some(&(ref token @ Token::Return, _)) | Some(&(ref token @ Token::Throw, _)) => { + let is_return = match token { + Token::Return => true, + Token::Throw => false, + _ => panic!(), + }; + input.next(); + match input.peek() { - Some(&(Token::SemiColon, pos)) => Ok(Stmt::Return(pos)), + // return; or throw; + Some(&(Token::SemiColon, pos)) => Ok(Stmt::ReturnWithVal(None, is_return, pos)), + // Just a return/throw without anything at the end of script + None => Ok(Stmt::ReturnWithVal(None, is_return, Position::eof())), + // return or throw with expression Some(&(_, pos)) => { let ret = parse_expr(input)?; - Ok(Stmt::ReturnWithVal(Box::new(ret), pos)) + Ok(Stmt::ReturnWithVal(Some(Box::new(ret)), is_return, pos)) } - _ => parse_expr_stmt(input), } } Some(&(Token::LeftBrace, _)) => parse_block(input), @@ -1859,6 +1873,7 @@ fn parse_top_level<'a>(input: &mut Peekable>) -> Result statements.push(parse_stmt(input)?), } + // Notice semicolons are optional if let Some(&(Token::SemiColon, _)) = input.peek() { input.next(); } diff --git a/tests/throw.rs b/tests/throw.rs new file mode 100644 index 00000000..ca14cb3f --- /dev/null +++ b/tests/throw.rs @@ -0,0 +1,18 @@ +use rhai::{Engine, EvalAltResult}; + +#[test] +fn test_throw() { + let mut engine = Engine::new(); + + match engine.eval::(r#"if true { throw "hello" }"#) { + Ok(_) => panic!("not an error"), + Err(EvalAltResult::ErrorRuntime(s, _)) if s == "hello" => (), + Err(err) => panic!("wrong error: {}", err), + } + + match engine.eval::(r#"throw;"#) { + Ok(_) => panic!("not an error"), + Err(EvalAltResult::ErrorRuntime(s, _)) if s == "" => (), + Err(err) => panic!("wrong error: {}", err), + } +}