From c917aa0a5adda8fea09d8d24a64a28b989a1158a Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 6 Mar 2020 23:49:52 +0800 Subject: [PATCH 01/27] Code cleanup. --- src/engine.rs | 120 +++++++++++++++++++++++--------------------------- 1 file changed, 54 insertions(+), 66 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index b00db1f9..d394c31d 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -19,7 +19,7 @@ const KEYWORD_DEBUG: &'static str = "debug"; const KEYWORD_TYPE_OF: &'static str = "type_of"; #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] -enum VariableType { +enum IndexSourceType { Array, String, Expression, @@ -283,25 +283,27 @@ impl Engine<'_> { val: Dynamic, idx: i64, pos: Position, - ) -> Result<(Dynamic, VariableType), EvalAltResult> { + ) -> Result<(Dynamic, IndexSourceType), EvalAltResult> { if val.is::() { + // val_array[idx] let arr = val.downcast::().expect("array expected"); if idx >= 0 { arr.get(idx as usize) .cloned() - .map(|v| (v, VariableType::Array)) + .map(|v| (v, IndexSourceType::Array)) .ok_or_else(|| EvalAltResult::ErrorArrayBounds(arr.len(), idx, pos)) } else { Err(EvalAltResult::ErrorArrayBounds(arr.len(), idx, pos)) } } else if val.is::() { + // val_string[idx] let s = val.downcast::().expect("string expected"); if idx >= 0 { s.chars() .nth(idx as usize) - .map(|ch| (ch.into_dynamic(), VariableType::String)) + .map(|ch| (ch.into_dynamic(), IndexSourceType::String)) .ok_or_else(|| EvalAltResult::ErrorStringBounds(s.chars().count(), idx, pos)) } else { Err(EvalAltResult::ErrorStringBounds( @@ -311,6 +313,7 @@ impl Engine<'_> { )) } } else { + // Error - cannot be indexed Err(EvalAltResult::ErrorIndexingType(pos)) } } @@ -321,7 +324,7 @@ impl Engine<'_> { scope: &mut Scope, lhs: &Expr, idx_expr: &Expr, - ) -> Result<(VariableType, Option<(String, usize)>, usize, Dynamic), EvalAltResult> { + ) -> Result<(IndexSourceType, Option<(String, usize)>, usize, Dynamic), EvalAltResult> { let idx = self.eval_index_value(scope, idx_expr)?; match lhs { @@ -332,58 +335,57 @@ impl Engine<'_> { |val| Self::get_indexed_value(val, idx, idx_expr.position()), lhs.position(), ) - .map(|(src_idx, (val, source_type))| { - (source_type, Some((id.clone(), src_idx)), idx as usize, val) + .map(|(src_idx, (val, src_type))| { + (src_type, Some((id.clone(), src_idx)), idx as usize, val) }), // (expr)[idx_expr] expr => Self::get_indexed_value(self.eval_expr(scope, expr)?, idx, idx_expr.position()) - .map(|(val, _)| (VariableType::Expression, None, idx as usize, val)), + .map(|(val, _)| (IndexSourceType::Expression, None, idx as usize, val)), } } /// Replace a character at an index position in a mutable string fn str_replace_char(s: &mut String, idx: usize, new_ch: char) { - // The new character - let ch = s.chars().nth(idx).expect("string index out of bounds"); + let mut chars: Vec = s.chars().collect(); + let ch = *chars.get(idx).expect("string index out of bounds"); // See if changed - if so, update the String - if ch == new_ch { - return; + if ch != new_ch { + chars[idx] = new_ch; + s.clear(); + chars.iter().for_each(|&ch| s.push(ch)); } - - // Collect all the characters after the index - let mut chars: Vec = s.chars().collect(); - chars[idx] = new_ch; - s.clear(); - chars.iter().for_each(|&ch| s.push(ch)); } /// Update the value at an index position in a variable inside the scope fn update_indexed_variable_in_scope( - source_type: VariableType, + src_type: IndexSourceType, scope: &mut Scope, id: &str, src_idx: usize, idx: usize, val: Dynamic, - ) -> Option { - match source_type { - VariableType::Array => { + ) -> Dynamic { + match src_type { + // array_id[idx] = val + IndexSourceType::Array => { let arr = scope.get_mut_by_type::(id, src_idx); - Some((arr[idx as usize] = val).into_dynamic()) + (arr[idx as usize] = val).into_dynamic() } - VariableType::String => { + // string_id[idx] = val + IndexSourceType::String => { let s = scope.get_mut_by_type::(id, src_idx); // Value must be a character let ch = *val .downcast::() .expect("char value expected to update an index position in a string"); - Some(Self::str_replace_char(s, idx as usize, ch).into_dynamic()) + Self::str_replace_char(s, idx as usize, ch).into_dynamic() } - _ => None, + // All other variable types should be an error + _ => panic!("array or string source type expected for indexing"), } } @@ -397,19 +399,19 @@ impl Engine<'_> { match dot_lhs { // xxx.??? Expr::Identifier(id, pos) => { - let (sc_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?; + let (src_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?; let value = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); // In case the expression mutated `target`, we need to reassign it because // of the above `clone`. - *scope.get_mut(id, sc_idx) = target; + *scope.get_mut(id, src_idx) = target; value } // lhs[idx_expr].??? Expr::Index(lhs, idx_expr) => { - let (source_type, src, idx, mut target) = + let (src_type, src, idx, mut target) = self.eval_index_expr(scope, lhs, idx_expr)?; let value = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); @@ -417,14 +419,8 @@ impl Engine<'_> { // of the above `clone`. if let Some((id, src_idx)) = src { Self::update_indexed_variable_in_scope( - source_type, - scope, - &id, - src_idx, - idx, - target, - ) - .expect("array or string source type expected for indexing"); + src_type, scope, &id, src_idx, idx, target, + ); } value @@ -435,8 +431,6 @@ impl Engine<'_> { let mut target = self.eval_expr(scope, expr)?; self.get_dot_val_helper(scope, target.as_mut(), dot_rhs) } - // Syntax error - //_ => Err(EvalAltResult::ErrorDotExpr(dot_lhs.position())), } } @@ -495,19 +489,19 @@ impl Engine<'_> { match dot_lhs { // id.??? Expr::Identifier(id, pos) => { - let (sc_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?; + let (src_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?; let value = self.set_dot_val_helper(target.as_mut(), dot_rhs, source_val); // In case the expression mutated `target`, we need to reassign it because // of the above `clone`. - *scope.get_mut(id, sc_idx) = target; + *scope.get_mut(id, src_idx) = target; value } // lhs[idx_expr].??? Expr::Index(lhs, idx_expr) => { - let (source_type, src, idx, mut target) = + let (src_type, src, idx, mut target) = self.eval_index_expr(scope, lhs, idx_expr)?; let value = self.set_dot_val_helper(target.as_mut(), dot_rhs, source_val); @@ -516,14 +510,8 @@ impl Engine<'_> { if let Some((id, src_idx)) = src { Self::update_indexed_variable_in_scope( - source_type, - scope, - &id, - src_idx, - idx, - target, - ) - .expect("array or string source_type expected for indexing"); + src_type, scope, &id, src_idx, idx, target, + ); } value @@ -565,24 +553,18 @@ impl Engine<'_> { // idx_lhs[idx_expr] = rhs Expr::Index(idx_lhs, idx_expr) => { - let (source_type, src, idx, _) = + let (src_type, src, idx, _) = self.eval_index_expr(scope, idx_lhs, idx_expr)?; if let Some((id, src_idx)) = src { - Self::update_indexed_variable_in_scope( - source_type, - scope, - &id, - src_idx, - idx, - rhs_val, - ) + Ok(Self::update_indexed_variable_in_scope( + src_type, scope, &id, src_idx, idx, rhs_val, + )) } else { - None + Err(EvalAltResult::ErrorAssignmentToUnknownLHS( + idx_lhs.position(), + )) } - .ok_or_else(|| { - EvalAltResult::ErrorAssignmentToUnknownLHS(idx_lhs.position()) - }) } // dot_lhs.dot_rhs = rhs @@ -670,8 +652,10 @@ impl Engine<'_> { stmt: &Stmt, ) -> Result { match stmt { + // Expression as statement Stmt::Expr(expr) => self.eval_expr(scope, expr), + // Block scope Stmt::Block(block) => { let prev_len = scope.len(); let mut last_result: Result = Ok(().into_dynamic()); @@ -685,13 +669,12 @@ impl Engine<'_> { } } - while scope.len() > prev_len { - scope.pop(); - } + scope.rewind(prev_len); last_result } + // If-else statement Stmt::IfElse(guard, body, else_body) => self .eval_expr(scope, guard)? .downcast::() @@ -706,6 +689,7 @@ impl Engine<'_> { } }), + // While loop Stmt::While(guard, body) => loop { match self.eval_expr(scope, guard)?.downcast::() { Ok(guard_val) => { @@ -723,6 +707,7 @@ impl Engine<'_> { } }, + // Loop statement Stmt::Loop(body) => loop { match self.eval_stmt(scope, body) { Err(EvalAltResult::LoopBreak) => return Ok(().into_dynamic()), @@ -731,6 +716,7 @@ impl Engine<'_> { } }, + // For loop Stmt::For(name, expr, body) => { let arr = self.eval_expr(scope, expr)?; let tid = Any::type_id(&*arr); @@ -755,6 +741,7 @@ impl Engine<'_> { } } + // Break statement Stmt::Break(_) => Err(EvalAltResult::LoopBreak), // Empty return @@ -783,6 +770,7 @@ impl Engine<'_> { )) } + // Let statement Stmt::Let(name, init, _) => { if let Some(v) = init { let val = self.eval_expr(scope, v)?; From ea82ee81d6de6f239f70375a2734e71d4429f655 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 7 Mar 2020 00:29:45 +0800 Subject: [PATCH 02/27] Fix call_fn to take &ast instead of ast. --- README.md | 2 +- src/api.rs | 12 ++++++------ tests/engine.rs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c78534a0..a379b829 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ let ast = Engine::compile("fn hello(x, y) { x.len() + y }")?; // Evaluate the function in the AST, passing arguments into the script as a tuple // (beware, arguments must be of the correct types because Rhai does not have built-in type conversions) -let result: i64 = engine.call_fn("hello", ast, (&mut String::from("abc"), &mut 123_i64))?; +let result: i64 = engine.call_fn("hello", &ast, (&mut String::from("abc"), &mut 123_i64))?; ``` # Values and types diff --git a/src/api.rs b/src/api.rs index b27378f2..d38dad9a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -9,7 +9,7 @@ use crate::scope::Scope; use std::any::TypeId; use std::sync::Arc; -impl<'a> Engine<'a> { +impl<'e> Engine<'e> { pub(crate) fn register_fn_raw( &mut self, fn_name: &str, @@ -169,7 +169,7 @@ impl<'a> Engine<'a> { let result = statements .iter() - .try_fold(().into_dynamic(), |_, o| self.eval_stmt(scope, o)); + .try_fold(().into_dynamic(), |_, stmt| self.eval_stmt(scope, stmt)); self.script_functions.clear(); // Clean up engine @@ -261,7 +261,7 @@ impl<'a> Engine<'a> { /// /// let ast = Engine::compile("fn add(x, y) { x.len() + y }")?; /// - /// let result: i64 = engine.call_fn("add", ast, (&mut String::from("abc"), &mut 123_i64))?; + /// let result: i64 = engine.call_fn("add", &ast, (&mut String::from("abc"), &mut 123_i64))?; /// /// assert_eq!(result, 126); /// # Ok(()) @@ -270,7 +270,7 @@ impl<'a> Engine<'a> { pub fn call_fn<'f, A: FuncArgs<'f>, T: Any + Clone>( &mut self, name: &str, - ast: AST, + ast: &AST, args: A, ) -> Result { let pos = Default::default(); @@ -317,7 +317,7 @@ impl<'a> Engine<'a> { /// } /// assert_eq!(result, "42"); /// ``` - pub fn on_print(&mut self, callback: impl FnMut(&str) + 'a) { + pub fn on_print(&mut self, callback: impl FnMut(&str) + 'e) { self.on_print = Box::new(callback); } @@ -337,7 +337,7 @@ impl<'a> Engine<'a> { /// } /// assert_eq!(result, "\"hello\""); /// ``` - pub fn on_debug(&mut self, callback: impl FnMut(&str) + 'a) { + pub fn on_debug(&mut self, callback: impl FnMut(&str) + 'e) { self.on_debug = Box::new(callback); } } diff --git a/tests/engine.rs b/tests/engine.rs index 082daaa4..8db972c4 100644 --- a/tests/engine.rs +++ b/tests/engine.rs @@ -6,7 +6,7 @@ fn test_engine_call_fn() -> Result<(), EvalAltResult> { let ast = Engine::compile("fn hello(x, y) { x.len() + y }")?; - let result: i64 = engine.call_fn("hello", ast, (&mut String::from("abc"), &mut 123_i64))?; + let result: i64 = engine.call_fn("hello", &ast, (&mut String::from("abc"), &mut 123_i64))?; assert_eq!(result, 126); From 024133ae2db0f13563ddfff969181af90445202b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 7 Mar 2020 10:15:42 +0800 Subject: [PATCH 03/27] Avoid string copying. --- src/engine.rs | 68 +++++++++++++++++++++-------------------- src/error.rs | 3 ++ src/parser.rs | 76 +++++++++++++++++++++++++++------------------- src/scope.rs | 35 ++++++++++++--------- tests/var_scope.rs | 4 +-- 5 files changed, 106 insertions(+), 80 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index d394c31d..53cb3ddc 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -46,22 +46,22 @@ type IteratorFn = dyn Fn(&Dynamic) -> Box>; /// } /// } /// ``` -pub struct Engine<'a> { +pub struct Engine<'e> { /// A hashmap containing all compiled functions known to the engine - pub(crate) external_functions: HashMap, Arc>, + pub(crate) external_functions: HashMap, Arc>>, /// A hashmap containing all script-defined functions - pub(crate) script_functions: HashMap, Arc>, + pub(crate) script_functions: HashMap, Arc>>, /// A hashmap containing all iterators known to the engine pub(crate) type_iterators: HashMap>, pub(crate) type_names: HashMap, - pub(crate) on_print: Box, - pub(crate) on_debug: Box, + pub(crate) on_print: Box, + pub(crate) on_debug: Box, } -pub enum FnIntExt { +pub enum FnIntExt<'a> { Ext(Box), - Int(FnDef), + Int(FnDef<'a>), } pub type FnAny = dyn Fn(FnCallArgs, Position) -> Result; @@ -104,27 +104,27 @@ impl Engine<'_> { if let Some(f) = fn_def { match *f { - FnIntExt::Ext(ref f) => { - let r = f(args, pos)?; + FnIntExt::Ext(ref func) => { + let result = func(args, pos)?; let callback = match spec.name.as_ref() { KEYWORD_PRINT => self.on_print.as_mut(), KEYWORD_DEBUG => self.on_debug.as_mut(), - _ => return Ok(r), + _ => return Ok(result), }; - Ok(callback( - &r.downcast::() - .map(|s| *s) - .unwrap_or("error: not a string".into()), - ) - .into_dynamic()) + let val = &result + .downcast::() + .map(|s| *s) + .unwrap_or("error: not a string".into()); + + Ok(callback(val).into_dynamic()) } - FnIntExt::Int(ref f) => { - if f.params.len() != args.len() { + FnIntExt::Int(ref func) => { + if func.params.len() != args.len() { return Err(EvalAltResult::ErrorFunctionArgsMismatch( spec.name.into(), - f.params.len(), + func.params.len(), args.len(), pos, )); @@ -133,13 +133,13 @@ impl Engine<'_> { let mut scope = Scope::new(); scope.extend( - f.params + func.params .iter() - .cloned() + .map(|s| s.clone()) .zip(args.iter().map(|x| (*x).into_dynamic())), ); - match self.eval_stmt(&mut scope, &*f.body) { + match self.eval_stmt(&mut scope, &*func.body) { Err(EvalAltResult::Return(x, _)) => Ok(x), other => other, } @@ -319,12 +319,12 @@ impl Engine<'_> { } /// Evaluate an index expression - fn eval_index_expr( + fn eval_index_expr<'a>( &mut self, scope: &mut Scope, - lhs: &Expr, + lhs: &'a Expr, idx_expr: &Expr, - ) -> Result<(IndexSourceType, Option<(String, usize)>, usize, Dynamic), EvalAltResult> { + ) -> Result<(IndexSourceType, Option<(&'a str, usize)>, usize, Dynamic), EvalAltResult> { let idx = self.eval_index_value(scope, idx_expr)?; match lhs { @@ -336,7 +336,7 @@ impl Engine<'_> { lhs.position(), ) .map(|(src_idx, (val, src_type))| { - (src_type, Some((id.clone(), src_idx)), idx as usize, val) + (src_type, Some((id.as_str(), src_idx)), idx as usize, val) }), // (expr)[idx_expr] @@ -371,7 +371,8 @@ impl Engine<'_> { // array_id[idx] = val IndexSourceType::Array => { let arr = scope.get_mut_by_type::(id, src_idx); - (arr[idx as usize] = val).into_dynamic() + arr[idx as usize] = val; + ().into_dynamic() } // string_id[idx] = val @@ -381,7 +382,8 @@ impl Engine<'_> { let ch = *val .downcast::() .expect("char value expected to update an index position in a string"); - Self::str_replace_char(s, idx as usize, ch).into_dynamic() + Self::str_replace_char(s, idx as usize, ch); + ().into_dynamic() } // All other variable types should be an error @@ -419,7 +421,7 @@ impl Engine<'_> { // of the above `clone`. if let Some((id, src_idx)) = src { Self::update_indexed_variable_in_scope( - src_type, scope, &id, src_idx, idx, target, + src_type, scope, id, src_idx, idx, target, ); } @@ -510,7 +512,7 @@ impl Engine<'_> { if let Some((id, src_idx)) = src { Self::update_indexed_variable_in_scope( - src_type, scope, &id, src_idx, idx, target, + src_type, scope, id, src_idx, idx, target, ); } @@ -525,10 +527,10 @@ impl Engine<'_> { /// Evaluate an expression fn eval_expr(&mut self, scope: &mut Scope, expr: &Expr) -> Result { match expr { - Expr::IntegerConstant(i, _) => Ok((*i).into_dynamic()), - Expr::FloatConstant(i, _) => Ok((*i).into_dynamic()), + Expr::IntegerConstant(i, _) => Ok(i.into_dynamic()), + Expr::FloatConstant(f, _) => Ok(f.into_dynamic()), Expr::StringConstant(s, _) => Ok(s.into_dynamic()), - Expr::CharConstant(c, _) => Ok((*c).into_dynamic()), + Expr::CharConstant(c, _) => Ok(c.into_dynamic()), Expr::Identifier(id, pos) => { Self::search_scope(scope, id, Ok, *pos).map(|(_, val)| val) } diff --git a/src/error.rs b/src/error.rs index 3096040e..52ca89fc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -75,6 +75,8 @@ pub enum ParseErrorType { FnMissingName, /// A function definition is missing the parameters list. Wrapped value is the function name. FnMissingParams(String), + /// Assignment to an inappropriate LHS (left-hand-side) expression. + AssignmentToInvalidLHS, } /// Error when parsing a script. @@ -114,6 +116,7 @@ impl Error for ParseError { ParseErrorType::FnMissingName => "Expecting name in function declaration", ParseErrorType::FnMissingParams(_) => "Expecting parameters in function declaration", ParseErrorType::WrongFnDefinition => "Function definitions must be at top level and cannot be inside a block or another function", + ParseErrorType::AssignmentToInvalidLHS => "Assignment to an unsupported left-hand side expression" } } diff --git a/src/parser.rs b/src/parser.rs index 204998b0..18052ee9 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,7 +2,7 @@ use crate::any::Dynamic; use crate::error::{LexError, ParseError, ParseErrorType}; use std::char; use std::iter::Peekable; -use std::str::Chars; +use std::{borrow::Cow, str::Chars}; type LERR = LexError; type PERR = ParseErrorType; @@ -99,12 +99,12 @@ impl std::fmt::Debug for Position { } /// Compiled AST (abstract syntax tree) of a Rhai script. -pub struct AST(pub(crate) Vec, pub(crate) Vec); +pub struct AST(pub(crate) Vec, pub(crate) Vec>); #[derive(Debug, Clone)] -pub struct FnDef { - pub name: String, - pub params: Vec, +pub struct FnDef<'a> { + pub name: Cow<'a, str>, + pub params: Vec>, pub body: Box, pub pos: Position, } @@ -232,14 +232,14 @@ pub enum Token { } impl Token { - pub fn syntax(&self) -> std::borrow::Cow<'static, str> { + pub fn syntax<'a>(&'a self) -> Cow<'a, str> { use self::Token::*; match *self { - IntegerConstant(ref s) => s.to_string().into(), - FloatConstant(ref s) => s.to_string().into(), - Identifier(ref s) => s.to_string().into(), - CharConstant(ref s) => s.to_string().into(), + IntegerConstant(ref i) => i.to_string().into(), + FloatConstant(ref f) => f.to_string().into(), + Identifier(ref s) => s.into(), + CharConstant(ref c) => c.to_string().into(), LexError(ref err) => err.to_string().into(), ref token => (match token { @@ -301,7 +301,7 @@ impl Token { PowerOfAssign => "~=", For => "for", In => "in", - _ => panic!(), + _ => panic!("operator should be match in outer scope"), }) .into(), } @@ -538,8 +538,7 @@ impl<'a> TokenIterator<'a> { } } - let out: String = result.iter().collect(); - Ok(out) + Ok(result.iter().collect()) } fn inner_next(&mut self) -> Option<(Token, Position)> { @@ -640,20 +639,20 @@ impl<'a> TokenIterator<'a> { }, pos, )); + } else { + let out: String = result.iter().filter(|&&c| c != '_').collect(); + + return Some(( + if let Ok(val) = out.parse::() { + Token::IntegerConstant(val) + } else if let Ok(val) = out.parse::() { + Token::FloatConstant(val) + } else { + Token::LexError(LERR::MalformedNumber(result.iter().collect())) + }, + pos, + )); } - - let out: String = result.iter().filter(|&&c| c != '_').collect(); - - return Some(( - if let Ok(val) = out.parse::() { - Token::IntegerConstant(val) - } else if let Ok(val) = out.parse::() { - Token::FloatConstant(val) - } else { - Token::LexError(LERR::MalformedNumber(result.iter().collect())) - }, - pos, - )); } 'A'..='Z' | 'a'..='z' | '_' => { let mut result = Vec::new(); @@ -687,7 +686,7 @@ impl<'a> TokenIterator<'a> { "fn" => Token::Fn, "for" => Token::For, "in" => Token::In, - x => Token::Identifier(x.into()), + _ => Token::Identifier(out), }, pos, )); @@ -1264,6 +1263,21 @@ fn parse_unary<'a>(input: &mut Peekable>) -> Result Result { + match lhs { + // Only assignments to a variable, and index erxpression and a dot expression is valid LHS + Expr::Identifier(_, _) | Expr::Index(_, _) | Expr::Dot(_, _) => { + Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs))) + } + + // All other LHS cannot be assigned to + _ => Err(ParseError::new( + PERR::AssignmentToInvalidLHS, + lhs.position(), + )), + } +} + fn parse_binary_op<'a>( input: &mut Peekable>, precedence: i8, @@ -1308,7 +1322,7 @@ fn parse_binary_op<'a>( } Token::Divide => Expr::FunctionCall("/".into(), vec![current_lhs, rhs], None, pos), - Token::Equals => Expr::Assignment(Box::new(current_lhs), Box::new(rhs)), + Token::Equals => parse_assignment(current_lhs, rhs)?, Token::PlusAssign => { let lhs_copy = current_lhs.clone(); Expr::Assignment( @@ -1687,7 +1701,7 @@ fn parse_stmt<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { +fn parse_fn<'a>(input: &mut Peekable>) -> Result, ParseError> { let pos = match input.next() { Some((_, tok_pos)) => tok_pos, _ => return Err(ParseError::new(PERR::InputPastEndOfFile, Position::eof())), @@ -1723,7 +1737,7 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result break, Some((Token::Comma, _)) => (), Some((Token::Identifier(s), _)) => { - params.push(s); + params.push(s.into()); } Some((_, pos)) => return Err(ParseError::new(PERR::MalformedCallExpr, pos)), None => return Err(ParseError::new(PERR::MalformedCallExpr, Position::eof())), @@ -1734,7 +1748,7 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result); +pub struct Scope<'a>(Vec<(Cow<'a, str>, Dynamic)>); -impl Scope { +impl<'a> Scope<'a> { /// Create a new Scope. pub fn new() -> Self { Self(Vec::new()) @@ -36,18 +37,18 @@ impl Scope { } /// Add (push) a new variable to the Scope. - pub fn push(&mut self, key: String, value: T) { - self.0.push((key, Box::new(value))); + pub fn push>, T: Any>(&mut self, key: K, value: T) { + self.0.push((key.into(), Box::new(value))); } /// Add (push) a new variable to the Scope. - pub(crate) fn push_dynamic(&mut self, key: String, value: Dynamic) { - self.0.push((key, value)); + pub(crate) fn push_dynamic>>(&mut self, key: K, value: Dynamic) { + self.0.push((key.into(), value)); } /// Remove (pop) the last variable from the Scope. pub fn pop(&mut self) -> Option<(String, Dynamic)> { - self.0.pop() + self.0.pop().map(|(key, value)| (key.to_string(), value)) } /// Truncate (rewind) the Scope to a previous size. @@ -56,13 +57,13 @@ impl Scope { } /// Find a variable in the Scope, starting from the last. - pub fn get(&self, key: &str) -> Option<(usize, String, Dynamic)> { + pub fn get(&self, key: &str) -> Option<(usize, &str, Dynamic)> { self.0 .iter() .enumerate() .rev() // Always search a Scope in reverse order .find(|(_, (name, _))| name == key) - .map(|(i, (name, value))| (i, name.clone(), value.clone())) + .map(|(i, (name, value))| (i, name.as_ref(), value.clone())) } /// Get the value of a variable in the Scope, starting from the last. @@ -97,20 +98,26 @@ impl Scope { self.0 .iter() .rev() // Always search a Scope in reverse order - .map(|(key, value)| (key.as_str(), value)) + .map(|(key, value)| (key.as_ref(), value)) } + /* /// Get a mutable iterator to variables in the Scope. pub fn iter_mut(&mut self) -> impl Iterator { self.0 .iter_mut() .rev() // Always search a Scope in reverse order - .map(|(key, value)| (key.as_str(), value)) + .map(|(key, value)| (key.as_ref(), value)) } + */ } -impl std::iter::Extend<(String, Dynamic)> for Scope { - fn extend>(&mut self, iter: T) { - self.0.extend(iter); +impl<'a, K> std::iter::Extend<(K, Dynamic)> for Scope<'a> +where + K: Into>, +{ + fn extend>(&mut self, iter: T) { + self.0 + .extend(iter.into_iter().map(|(key, value)| (key.into(), value))); } } diff --git a/tests/var_scope.rs b/tests/var_scope.rs index 5522ba67..d1f0c24e 100644 --- a/tests/var_scope.rs +++ b/tests/var_scope.rs @@ -25,8 +25,8 @@ fn test_scope_eval() -> Result<(), EvalAltResult> { // Then push some initialized variables into the state // NOTE: Remember the default numbers used by Rhai are i64 and f64. // Better stick to them or it gets hard to work with other variables in the script. - scope.push("y".into(), 42_i64); - scope.push("z".into(), 999_i64); + scope.push("y", 42_i64); + scope.push("z", 999_i64); // First invocation engine From 473e40e8a43e50d975d2e4f56f38b74c0be333df Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 7 Mar 2020 10:16:20 +0800 Subject: [PATCH 04/27] Catch more invalid LHS for assignments. --- src/engine.rs | 62 ++++++++------ src/parser.rs | 226 +++++++++++++++++++++++--------------------------- 2 files changed, 138 insertions(+), 150 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 53cb3ddc..c0ccad18 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -199,7 +199,7 @@ impl Engine<'_> { } // xxx.lhs[idx_expr] - Expr::Index(lhs, idx_expr) => { + Expr::Index(lhs, idx_expr, idx_pos) => { let idx = self.eval_index_value(scope, idx_expr)?; let (lhs_value, _) = match lhs.as_ref() { @@ -213,11 +213,12 @@ impl Engine<'_> { expr => return Err(EvalAltResult::ErrorDotExpr(expr.position())), }; - Self::get_indexed_value(lhs_value, idx, idx_expr.position()).map(|(v, _)| v) + Self::get_indexed_value(lhs_value, idx, idx_expr.position(), *idx_pos) + .map(|(v, _)| v) } // xxx.lhs.rhs - Expr::Dot(lhs, rhs) => match lhs.as_ref() { + Expr::Dot(lhs, rhs, _) => match lhs.as_ref() { // xxx.id.rhs Expr::Identifier(id, pos) => { let get_fn_name = format!("get${}", id); @@ -226,7 +227,7 @@ impl Engine<'_> { .and_then(|mut v| self.get_dot_val_helper(scope, v.as_mut(), rhs)) } // xxx.lhs[idx_expr].rhs - Expr::Index(lhs, idx_expr) => { + Expr::Index(lhs, idx_expr, idx_pos) => { let idx = self.eval_index_value(scope, idx_expr)?; let (lhs_value, _) = match lhs.as_ref() { @@ -240,7 +241,7 @@ impl Engine<'_> { expr => return Err(EvalAltResult::ErrorDotExpr(expr.position())), }; - Self::get_indexed_value(lhs_value, idx, idx_expr.position()).and_then( + Self::get_indexed_value(lhs_value, idx, idx_expr.position(), *idx_pos).and_then( |(mut value, _)| self.get_dot_val_helper(scope, value.as_mut(), rhs), ) } @@ -282,7 +283,8 @@ impl Engine<'_> { fn get_indexed_value( val: Dynamic, idx: i64, - pos: Position, + val_pos: Position, + idx_pos: Position, ) -> Result<(Dynamic, IndexSourceType), EvalAltResult> { if val.is::() { // val_array[idx] @@ -292,9 +294,9 @@ impl Engine<'_> { arr.get(idx as usize) .cloned() .map(|v| (v, IndexSourceType::Array)) - .ok_or_else(|| EvalAltResult::ErrorArrayBounds(arr.len(), idx, pos)) + .ok_or_else(|| EvalAltResult::ErrorArrayBounds(arr.len(), idx, val_pos)) } else { - Err(EvalAltResult::ErrorArrayBounds(arr.len(), idx, pos)) + Err(EvalAltResult::ErrorArrayBounds(arr.len(), idx, val_pos)) } } else if val.is::() { // val_string[idx] @@ -304,17 +306,19 @@ impl Engine<'_> { s.chars() .nth(idx as usize) .map(|ch| (ch.into_dynamic(), IndexSourceType::String)) - .ok_or_else(|| EvalAltResult::ErrorStringBounds(s.chars().count(), idx, pos)) + .ok_or_else(|| { + EvalAltResult::ErrorStringBounds(s.chars().count(), idx, val_pos) + }) } else { Err(EvalAltResult::ErrorStringBounds( s.chars().count(), idx, - pos, + val_pos, )) } } else { // Error - cannot be indexed - Err(EvalAltResult::ErrorIndexingType(pos)) + Err(EvalAltResult::ErrorIndexingType(idx_pos)) } } @@ -324,6 +328,7 @@ impl Engine<'_> { scope: &mut Scope, lhs: &'a Expr, idx_expr: &Expr, + idx_pos: Position, ) -> Result<(IndexSourceType, Option<(&'a str, usize)>, usize, Dynamic), EvalAltResult> { let idx = self.eval_index_value(scope, idx_expr)?; @@ -332,7 +337,7 @@ impl Engine<'_> { Expr::Identifier(id, _) => Self::search_scope( scope, &id, - |val| Self::get_indexed_value(val, idx, idx_expr.position()), + |val| Self::get_indexed_value(val, idx, idx_expr.position(), idx_pos), lhs.position(), ) .map(|(src_idx, (val, src_type))| { @@ -340,8 +345,13 @@ impl Engine<'_> { }), // (expr)[idx_expr] - expr => Self::get_indexed_value(self.eval_expr(scope, expr)?, idx, idx_expr.position()) - .map(|(val, _)| (IndexSourceType::Expression, None, idx as usize, val)), + expr => Self::get_indexed_value( + self.eval_expr(scope, expr)?, + idx, + idx_expr.position(), + idx_pos, + ) + .map(|(val, _)| (IndexSourceType::Expression, None, idx as usize, val)), } } @@ -412,9 +422,9 @@ impl Engine<'_> { } // lhs[idx_expr].??? - Expr::Index(lhs, idx_expr) => { + Expr::Index(lhs, idx_expr, idx_pos) => { let (src_type, src, idx, mut target) = - self.eval_index_expr(scope, lhs, idx_expr)?; + self.eval_index_expr(scope, lhs, idx_expr, *idx_pos)?; let value = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); // In case the expression mutated `target`, we need to reassign it because @@ -457,7 +467,7 @@ impl Engine<'_> { } // xxx.lhs.rhs - Expr::Dot(lhs, rhs) => match lhs.as_ref() { + Expr::Dot(lhs, rhs, _) => match lhs.as_ref() { Expr::Identifier(id, pos) => { let get_fn_name = format!("get${}", id); @@ -502,9 +512,9 @@ impl Engine<'_> { } // lhs[idx_expr].??? - Expr::Index(lhs, idx_expr) => { + Expr::Index(lhs, idx_expr, idx_pos) => { let (src_type, src, idx, mut target) = - self.eval_index_expr(scope, lhs, idx_expr)?; + self.eval_index_expr(scope, lhs, idx_expr, *idx_pos)?; let value = self.set_dot_val_helper(target.as_mut(), dot_rhs, source_val); // In case the expression mutated `target`, we need to reassign it because @@ -534,12 +544,12 @@ impl Engine<'_> { Expr::Identifier(id, pos) => { Self::search_scope(scope, id, Ok, *pos).map(|(_, val)| val) } - Expr::Index(lhs, idx_expr) => self - .eval_index_expr(scope, lhs, idx_expr) + Expr::Index(lhs, idx_expr, idx_pos) => self + .eval_index_expr(scope, lhs, idx_expr, *idx_pos) .map(|(_, _, _, x)| x), // lhs = rhs - Expr::Assignment(lhs, rhs) => { + Expr::Assignment(lhs, rhs, _) => { let rhs_val = self.eval_expr(scope, rhs)?; match lhs.as_ref() { @@ -554,9 +564,9 @@ impl Engine<'_> { } // idx_lhs[idx_expr] = rhs - Expr::Index(idx_lhs, idx_expr) => { + Expr::Index(idx_lhs, idx_expr, idx_pos) => { let (src_type, src, idx, _) = - self.eval_index_expr(scope, idx_lhs, idx_expr)?; + self.eval_index_expr(scope, idx_lhs, idx_expr, *idx_pos)?; if let Some((id, src_idx)) = src { Ok(Self::update_indexed_variable_in_scope( @@ -570,7 +580,7 @@ impl Engine<'_> { } // dot_lhs.dot_rhs = rhs - Expr::Dot(dot_lhs, dot_rhs) => { + Expr::Dot(dot_lhs, dot_rhs, _) => { self.set_dot_val(scope, dot_lhs, dot_rhs, rhs_val) } @@ -579,7 +589,7 @@ impl Engine<'_> { } } - Expr::Dot(lhs, rhs) => self.get_dot_val(scope, lhs, rhs), + Expr::Dot(lhs, rhs, _) => self.get_dot_val(scope, lhs, rhs), Expr::Array(contents, _) => { let mut arr = Vec::new(); diff --git a/src/parser.rs b/src/parser.rs index 18052ee9..d01ecdce 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -130,9 +130,9 @@ pub enum Expr { CharConstant(char, Position), StringConstant(String, Position), FunctionCall(String, Vec, Option, Position), - Assignment(Box, Box), - Dot(Box, Box), - Index(Box, Box), + Assignment(Box, Box, Position), + Dot(Box, Box, Position), + Index(Box, Box, Position), Array(Vec, Position), And(Box, Box), Or(Box, Box), @@ -155,9 +155,9 @@ impl Expr { | Expr::False(pos) | Expr::Unit(pos) => *pos, - Expr::Index(e, _) - | Expr::Assignment(e, _) - | Expr::Dot(e, _) + Expr::Index(e, _, _) + | Expr::Assignment(e, _, _) + | Expr::Dot(e, _, _) | Expr::And(e, _) | Expr::Or(e, _) => e.position(), } @@ -1103,11 +1103,12 @@ fn parse_call_expr<'a>( fn parse_index_expr<'a>( lhs: Box, input: &mut Peekable>, + pos: Position, ) -> Result { parse_expr(input).and_then(|idx_expr| match input.peek() { Some(&(Token::RightBracket, _)) => { input.next(); - return Ok(Expr::Index(lhs, Box::new(idx_expr))); + return Ok(Expr::Index(lhs, Box::new(idx_expr), pos)); } Some(&(_, pos)) => { return Err(ParseError::new( @@ -1134,9 +1135,9 @@ fn parse_ident_expr<'a>( input.next(); parse_call_expr(id, input, begin) } - Some(&(Token::LeftBracket, _)) => { + Some(&(Token::LeftBracket, pos)) => { input.next(); - parse_index_expr(Box::new(Expr::Identifier(id, begin)), input) + parse_index_expr(Box::new(Expr::Identifier(id, begin)), input, pos) } Some(_) => Ok(Expr::Identifier(id, begin)), None => Ok(Expr::Identifier(id, Position::eof())), @@ -1225,9 +1226,9 @@ fn parse_primary<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result Result { - match lhs { - // Only assignments to a variable, and index erxpression and a dot expression is valid LHS - Expr::Identifier(_, _) | Expr::Index(_, _) | Expr::Dot(_, _) => { - Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs))) +fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result { + fn all_identifiers(expr: &Expr) -> (bool, Position) { + match expr { + // variable + Expr::Identifier(_, pos) => (true, *pos), + // indexing + Expr::Index(lhs, _, idx_pos) => match lhs.as_ref() { + // variable[x] + &Expr::Identifier(_, pos) => (true, pos), + // all other indexing is invalid + _ => (false, *idx_pos), + }, + // variable.prop.prop.prop... + Expr::Dot(lhs, rhs, _) => match lhs.as_ref() { + // variable.prop + &Expr::Identifier(_, pos) => { + let r = all_identifiers(rhs); + (r.0, if r.0 { pos } else { r.1 }) + } + // all other property access is invalid + _ => (false, lhs.position()), + }, + // everything else is invalid + _ => (false, expr.position()), } + } - // All other LHS cannot be assigned to - _ => Err(ParseError::new( - PERR::AssignmentToInvalidLHS, - lhs.position(), - )), + let r = all_identifiers(&lhs); + + if r.0 { + Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)) + } else { + Err(ParseError::new(PERR::AssignmentToInvalidLHS, r.1)) } } @@ -1322,32 +1344,24 @@ fn parse_binary_op<'a>( } Token::Divide => Expr::FunctionCall("/".into(), vec![current_lhs, rhs], None, pos), - Token::Equals => parse_assignment(current_lhs, rhs)?, + Token::Equals => parse_assignment(current_lhs, rhs, pos)?, Token::PlusAssign => { let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "+".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) + parse_assignment( + current_lhs, + Expr::FunctionCall("+".into(), vec![lhs_copy, rhs], None, pos), + pos, + )? } Token::MinusAssign => { let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "-".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) + parse_assignment( + current_lhs, + Expr::FunctionCall("-".into(), vec![lhs_copy, rhs], None, pos), + pos, + )? } - Token::Period => Expr::Dot(Box::new(current_lhs), Box::new(rhs)), + Token::Period => Expr::Dot(Box::new(current_lhs), Box::new(rhs), pos), // Comparison operators default to false when passed invalid operands Token::EqualsTo => Expr::FunctionCall( @@ -1392,63 +1406,43 @@ fn parse_binary_op<'a>( Token::XOr => Expr::FunctionCall("^".into(), vec![current_lhs, rhs], None, pos), Token::OrAssign => { let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "|".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) + parse_assignment( + current_lhs, + Expr::FunctionCall("|".into(), vec![lhs_copy, rhs], None, pos), + pos, + )? } Token::AndAssign => { let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "&".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) + parse_assignment( + current_lhs, + Expr::FunctionCall("&".into(), vec![lhs_copy, rhs], None, pos), + pos, + )? } Token::XOrAssign => { let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "^".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) + parse_assignment( + current_lhs, + Expr::FunctionCall("^".into(), vec![lhs_copy, rhs], None, pos), + pos, + )? } Token::MultiplyAssign => { let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "*".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) + parse_assignment( + current_lhs, + Expr::FunctionCall("*".into(), vec![lhs_copy, rhs], None, pos), + pos, + )? } Token::DivideAssign => { let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "/".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) + parse_assignment( + current_lhs, + Expr::FunctionCall("/".into(), vec![lhs_copy, rhs], None, pos), + pos, + )? } Token::Pipe => Expr::FunctionCall("|".into(), vec![current_lhs, rhs], None, pos), Token::LeftShift => { @@ -1459,27 +1453,19 @@ fn parse_binary_op<'a>( } Token::LeftShiftAssign => { let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "<<".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) + parse_assignment( + current_lhs, + Expr::FunctionCall("<<".into(), vec![lhs_copy, rhs], None, pos), + pos, + )? } Token::RightShiftAssign => { let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - ">>".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) + parse_assignment( + current_lhs, + Expr::FunctionCall(">>".into(), vec![lhs_copy, rhs], None, pos), + pos, + )? } Token::Ampersand => { Expr::FunctionCall("&".into(), vec![current_lhs, rhs], None, pos) @@ -1487,28 +1473,20 @@ fn parse_binary_op<'a>( Token::Modulo => Expr::FunctionCall("%".into(), vec![current_lhs, rhs], None, pos), Token::ModuloAssign => { let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "%".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) + parse_assignment( + current_lhs, + Expr::FunctionCall("%".into(), vec![lhs_copy, rhs], None, pos), + pos, + )? } Token::PowerOf => Expr::FunctionCall("~".into(), vec![current_lhs, rhs], None, pos), Token::PowerOfAssign => { let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "~".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) + parse_assignment( + current_lhs, + Expr::FunctionCall("~".into(), vec![lhs_copy, rhs], None, pos), + pos, + )? } token => { return Err(ParseError::new( @@ -1605,8 +1583,8 @@ fn parse_var<'a>(input: &mut Peekable>) -> Result { input.next(); - let initializer = parse_expr(input)?; - Ok(Stmt::Let(name, Some(Box::new(initializer)), pos)) + let init_value = parse_expr(input)?; + Ok(Stmt::Let(name, Some(Box::new(init_value)), pos)) } _ => Ok(Stmt::Let(name, None, pos)), } From 22cb69a16bd486160fda2bc75592e690bd08ea6f Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 7 Mar 2020 10:39:00 +0800 Subject: [PATCH 05/27] Allow block expressions. --- src/engine.rs | 5 +++++ src/parser.rs | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/engine.rs b/src/engine.rs index c0ccad18..5418a382 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -544,10 +544,15 @@ impl Engine<'_> { Expr::Identifier(id, pos) => { Self::search_scope(scope, id, Ok, *pos).map(|(_, val)| val) } + + // lhs[idx_expr] Expr::Index(lhs, idx_expr, idx_pos) => self .eval_index_expr(scope, lhs, idx_expr, *idx_pos) .map(|(_, _, _, x)| x), + // Statement block + Expr::Block(block, _) => self.eval_stmt(scope, block), + // lhs = rhs Expr::Assignment(lhs, rhs, _) => { let rhs_val = self.eval_expr(scope, rhs)?; diff --git a/src/parser.rs b/src/parser.rs index d01ecdce..328b496e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -129,6 +129,7 @@ pub enum Expr { Identifier(String, Position), CharConstant(char, Position), StringConstant(String, Position), + Block(Box, Position), FunctionCall(String, Vec, Option, Position), Assignment(Box, Box, Position), Dot(Box, Box, Position), @@ -150,6 +151,7 @@ impl Expr { | Expr::CharConstant(_, pos) | Expr::StringConstant(_, pos) | Expr::FunctionCall(_, _, _, pos) + | Expr::Block(_, pos) | Expr::Array(_, pos) | Expr::True(pos) | Expr::False(pos) @@ -1185,6 +1187,14 @@ fn parse_array_expr<'a>( } fn parse_primary<'a>(input: &mut Peekable>) -> Result { + // Block statement as expression + match input.peek() { + Some(&(Token::LeftBrace, pos)) => { + return parse_block(input).map(|block| Expr::Block(Box::new(block), pos)) + } + _ => (), + } + let token = input.next(); let mut follow_on = false; From a8be700b9f6ae93c431fe69db0a8a49962ca8542 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 7 Mar 2020 13:39:28 +0800 Subject: [PATCH 06/27] Simplify op-assignment. --- src/parser.rs | 137 ++++++++++++++++++-------------------------------- 1 file changed, 49 insertions(+), 88 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 328b496e..f6fa43df 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1310,6 +1310,44 @@ fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result Result { + let lhs_copy = lhs.clone(); + + parse_assignment( + lhs, + Expr::FunctionCall(function.into(), vec![lhs_copy, rhs], None, pos), + pos, + ) + + /* + const LHS_VALUE: &'static str = "@LHS_VALUE@"; + + let lhs_pos = lhs.position(); + + Ok(Expr::Block( + Box::new(Stmt::Block(vec![ + Stmt::Let(LHS_VALUE.to_string(), Some(Box::new(lhs)), lhs_pos), + Stmt::Expr(Box::new(parse_assignment( + lhs, + Expr::FunctionCall( + function.into(), + vec![Expr::Identifier(LHS_VALUE.to_string(), lhs_pos), rhs], + None, + pos, + ), + pos, + )?)), + ])), + pos, + )) + */ +} + fn parse_binary_op<'a>( input: &mut Peekable>, precedence: i8, @@ -1355,22 +1393,8 @@ fn parse_binary_op<'a>( Token::Divide => Expr::FunctionCall("/".into(), vec![current_lhs, rhs], None, pos), Token::Equals => parse_assignment(current_lhs, rhs, pos)?, - Token::PlusAssign => { - let lhs_copy = current_lhs.clone(); - parse_assignment( - current_lhs, - Expr::FunctionCall("+".into(), vec![lhs_copy, rhs], None, pos), - pos, - )? - } - Token::MinusAssign => { - let lhs_copy = current_lhs.clone(); - parse_assignment( - current_lhs, - Expr::FunctionCall("-".into(), vec![lhs_copy, rhs], None, pos), - pos, - )? - } + Token::PlusAssign => parse_op_assignment("+", current_lhs, rhs, pos)?, + Token::MinusAssign => parse_op_assignment("-", current_lhs, rhs, pos)?, Token::Period => Expr::Dot(Box::new(current_lhs), Box::new(rhs), pos), // Comparison operators default to false when passed invalid operands @@ -1414,46 +1438,11 @@ fn parse_binary_op<'a>( Token::Or => Expr::Or(Box::new(current_lhs), Box::new(rhs)), Token::And => Expr::And(Box::new(current_lhs), Box::new(rhs)), Token::XOr => Expr::FunctionCall("^".into(), vec![current_lhs, rhs], None, pos), - Token::OrAssign => { - let lhs_copy = current_lhs.clone(); - parse_assignment( - current_lhs, - Expr::FunctionCall("|".into(), vec![lhs_copy, rhs], None, pos), - pos, - )? - } - Token::AndAssign => { - let lhs_copy = current_lhs.clone(); - parse_assignment( - current_lhs, - Expr::FunctionCall("&".into(), vec![lhs_copy, rhs], None, pos), - pos, - )? - } - Token::XOrAssign => { - let lhs_copy = current_lhs.clone(); - parse_assignment( - current_lhs, - Expr::FunctionCall("^".into(), vec![lhs_copy, rhs], None, pos), - pos, - )? - } - Token::MultiplyAssign => { - let lhs_copy = current_lhs.clone(); - parse_assignment( - current_lhs, - Expr::FunctionCall("*".into(), vec![lhs_copy, rhs], None, pos), - pos, - )? - } - Token::DivideAssign => { - let lhs_copy = current_lhs.clone(); - parse_assignment( - current_lhs, - Expr::FunctionCall("/".into(), vec![lhs_copy, rhs], None, pos), - pos, - )? - } + Token::OrAssign => parse_op_assignment("|", current_lhs, rhs, pos)?, + Token::AndAssign => parse_op_assignment("&", current_lhs, rhs, pos)?, + Token::XOrAssign => parse_op_assignment("^", current_lhs, rhs, pos)?, + Token::MultiplyAssign => parse_op_assignment("*", current_lhs, rhs, pos)?, + Token::DivideAssign => parse_op_assignment("/", current_lhs, rhs, pos)?, Token::Pipe => Expr::FunctionCall("|".into(), vec![current_lhs, rhs], None, pos), Token::LeftShift => { Expr::FunctionCall("<<".into(), vec![current_lhs, rhs], None, pos) @@ -1461,43 +1450,15 @@ fn parse_binary_op<'a>( Token::RightShift => { Expr::FunctionCall(">>".into(), vec![current_lhs, rhs], None, pos) } - Token::LeftShiftAssign => { - let lhs_copy = current_lhs.clone(); - parse_assignment( - current_lhs, - Expr::FunctionCall("<<".into(), vec![lhs_copy, rhs], None, pos), - pos, - )? - } - Token::RightShiftAssign => { - let lhs_copy = current_lhs.clone(); - parse_assignment( - current_lhs, - Expr::FunctionCall(">>".into(), vec![lhs_copy, rhs], None, pos), - pos, - )? - } + Token::LeftShiftAssign => parse_op_assignment("<<", current_lhs, rhs, pos)?, + Token::RightShiftAssign => parse_op_assignment(">>", current_lhs, rhs, pos)?, Token::Ampersand => { Expr::FunctionCall("&".into(), vec![current_lhs, rhs], None, pos) } Token::Modulo => Expr::FunctionCall("%".into(), vec![current_lhs, rhs], None, pos), - Token::ModuloAssign => { - let lhs_copy = current_lhs.clone(); - parse_assignment( - current_lhs, - Expr::FunctionCall("%".into(), vec![lhs_copy, rhs], None, pos), - pos, - )? - } + Token::ModuloAssign => parse_op_assignment("%", current_lhs, rhs, pos)?, Token::PowerOf => Expr::FunctionCall("~".into(), vec![current_lhs, rhs], None, pos), - Token::PowerOfAssign => { - let lhs_copy = current_lhs.clone(); - parse_assignment( - current_lhs, - Expr::FunctionCall("~".into(), vec![lhs_copy, rhs], None, pos), - pos, - )? - } + Token::PowerOfAssign => parse_op_assignment("~", current_lhs, rhs, pos)?, token => { return Err(ParseError::new( PERR::UnknownOperator(token.syntax().into()), From d055638e83b2dd49a96dbe9f5bead0265d644005 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 7 Mar 2020 17:32:15 +0800 Subject: [PATCH 07/27] Properly detect invalid assignment LHS at compile time. --- src/engine.rs | 85 +++++++++++++++++++++++++++++++++++---------------- src/error.rs | 2 +- src/parser.rs | 47 +++++++++++++--------------- src/result.rs | 15 +++++---- 4 files changed, 90 insertions(+), 59 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 5418a382..bacd8982 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -17,6 +17,8 @@ pub type FnCallArgs<'a> = Vec<&'a mut Variant>; const KEYWORD_PRINT: &'static str = "print"; const KEYWORD_DEBUG: &'static str = "debug"; const KEYWORD_TYPE_OF: &'static str = "type_of"; +const FUNC_GETTER: &'static str = "get$"; +const FUNC_SETTER: &'static str = "set$"; #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] enum IndexSourceType { @@ -153,6 +155,12 @@ impl Engine<'_> { } else if let Some(val) = def_value { // Return default value Ok(val.clone()) + } else if spec.name.starts_with(FUNC_GETTER) || spec.name.starts_with(FUNC_SETTER) { + // Getter or setter + Err(EvalAltResult::ErrorDotExpr( + "- invalid property access".to_string(), + pos, + )) } else { let types_list = args .iter() @@ -193,7 +201,7 @@ impl Engine<'_> { // xxx.id Expr::Identifier(id, pos) => { - let get_fn_name = format!("get${}", id); + let get_fn_name = format!("{}{}", FUNC_GETTER, id); self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) } @@ -204,16 +212,18 @@ impl Engine<'_> { let (lhs_value, _) = match lhs.as_ref() { Expr::Identifier(id, pos) => { - let get_fn_name = format!("get${}", id); + let get_fn_name = format!("{}{}", FUNC_GETTER, id); ( self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)?, *pos, ) } - expr => return Err(EvalAltResult::ErrorDotExpr(expr.position())), + expr => { + return Err(EvalAltResult::ErrorDotExpr("".to_string(), expr.position())) + } }; - Self::get_indexed_value(lhs_value, idx, idx_expr.position(), *idx_pos) + self.get_indexed_value(lhs_value, idx, idx_expr.position(), *idx_pos) .map(|(v, _)| v) } @@ -221,7 +231,7 @@ impl Engine<'_> { Expr::Dot(lhs, rhs, _) => match lhs.as_ref() { // xxx.id.rhs Expr::Identifier(id, pos) => { - let get_fn_name = format!("get${}", id); + let get_fn_name = format!("{}{}", FUNC_GETTER, id); self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) .and_then(|mut v| self.get_dot_val_helper(scope, v.as_mut(), rhs)) @@ -232,25 +242,34 @@ impl Engine<'_> { let (lhs_value, _) = match lhs.as_ref() { Expr::Identifier(id, pos) => { - let get_fn_name = format!("get${}", id); + let get_fn_name = format!("{}{}", FUNC_GETTER, id); ( self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)?, *pos, ) } - expr => return Err(EvalAltResult::ErrorDotExpr(expr.position())), + expr => { + return Err(EvalAltResult::ErrorDotExpr( + "".to_string(), + expr.position(), + )) + } }; - Self::get_indexed_value(lhs_value, idx, idx_expr.position(), *idx_pos).and_then( - |(mut value, _)| self.get_dot_val_helper(scope, value.as_mut(), rhs), - ) + self.get_indexed_value(lhs_value, idx, idx_expr.position(), *idx_pos) + .and_then(|(mut value, _)| { + self.get_dot_val_helper(scope, value.as_mut(), rhs) + }) } // Syntax error - _ => Err(EvalAltResult::ErrorDotExpr(lhs.position())), + _ => Err(EvalAltResult::ErrorDotExpr("".to_string(), lhs.position())), }, // Syntax error - _ => Err(EvalAltResult::ErrorDotExpr(dot_rhs.position())), + _ => Err(EvalAltResult::ErrorDotExpr( + "".to_string(), + dot_rhs.position(), + )), } } @@ -281,6 +300,7 @@ impl Engine<'_> { /// Get the value at the indexed position of a base type fn get_indexed_value( + &self, val: Dynamic, idx: i64, val_pos: Position, @@ -318,7 +338,10 @@ impl Engine<'_> { } } else { // Error - cannot be indexed - Err(EvalAltResult::ErrorIndexingType(idx_pos)) + Err(EvalAltResult::ErrorIndexingType( + self.map_type_name(val.type_name()).to_string(), + idx_pos, + )) } } @@ -337,7 +360,7 @@ impl Engine<'_> { Expr::Identifier(id, _) => Self::search_scope( scope, &id, - |val| Self::get_indexed_value(val, idx, idx_expr.position(), idx_pos), + |val| self.get_indexed_value(val, idx, idx_expr.position(), idx_pos), lhs.position(), ) .map(|(src_idx, (val, src_type))| { @@ -345,13 +368,12 @@ impl Engine<'_> { }), // (expr)[idx_expr] - expr => Self::get_indexed_value( - self.eval_expr(scope, expr)?, - idx, - idx_expr.position(), - idx_pos, - ) - .map(|(val, _)| (IndexSourceType::Expression, None, idx as usize, val)), + expr => { + let val = self.eval_expr(scope, expr)?; + + self.get_indexed_value(val, idx, idx_expr.position(), idx_pos) + .map(|(value, _)| (IndexSourceType::Expression, None, idx as usize, value)) + } } } @@ -456,7 +478,7 @@ impl Engine<'_> { match dot_rhs { // xxx.id Expr::Identifier(id, pos) => { - let set_fn_name = format!("set${}", id); + let set_fn_name = format!("{}{}", FUNC_SETTER, id); self.call_fn_raw( &set_fn_name, @@ -469,7 +491,7 @@ impl Engine<'_> { // xxx.lhs.rhs Expr::Dot(lhs, rhs, _) => match lhs.as_ref() { Expr::Identifier(id, pos) => { - let get_fn_name = format!("get${}", id); + let get_fn_name = format!("{}{}", FUNC_GETTER, id); self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) .and_then(|mut v| { @@ -477,16 +499,22 @@ impl Engine<'_> { .map(|_| v) // Discard Ok return value }) .and_then(|mut v| { - let set_fn_name = format!("set${}", id); + let set_fn_name = format!("{}{}", FUNC_SETTER, id); self.call_fn_raw(&set_fn_name, vec![this_ptr, v.as_mut()], None, *pos) }) } - _ => Err(EvalAltResult::ErrorDotExpr(lhs.position())), + _ => Err(EvalAltResult::ErrorDotExpr( + "for assignment".to_string(), + lhs.position(), + )), }, // Syntax error - _ => Err(EvalAltResult::ErrorDotExpr(dot_rhs.position())), + _ => Err(EvalAltResult::ErrorDotExpr( + "for assignment".to_string(), + dot_rhs.position(), + )), } } @@ -530,7 +558,10 @@ impl Engine<'_> { } // Syntax error - _ => Err(EvalAltResult::ErrorDotExpr(dot_lhs.position())), + _ => Err(EvalAltResult::ErrorDotExpr( + "for assignment".to_string(), + dot_lhs.position(), + )), } } diff --git a/src/error.rs b/src/error.rs index 52ca89fc..d4096cb1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -116,7 +116,7 @@ impl Error for ParseError { ParseErrorType::FnMissingName => "Expecting name in function declaration", ParseErrorType::FnMissingParams(_) => "Expecting parameters in function declaration", ParseErrorType::WrongFnDefinition => "Function definitions must be at top level and cannot be inside a block or another function", - ParseErrorType::AssignmentToInvalidLHS => "Assignment to an unsupported left-hand side expression" + ParseErrorType::AssignmentToInvalidLHS => "Cannot assign to this expression because it will only be changing a copy of the value" } } diff --git a/src/parser.rs b/src/parser.rs index f6fa43df..5989b601 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1275,39 +1275,36 @@ fn parse_unary<'a>(input: &mut Peekable>) -> Result Result { - fn all_identifiers(expr: &Expr) -> (bool, Position) { + fn all_dots(expr: &Expr) -> (bool, Position) { match expr { - // variable Expr::Identifier(_, pos) => (true, *pos), - // indexing - Expr::Index(lhs, _, idx_pos) => match lhs.as_ref() { - // variable[x] - &Expr::Identifier(_, pos) => (true, pos), - // all other indexing is invalid - _ => (false, *idx_pos), + Expr::Dot(dot_lhs, dot_rhs, _) => match dot_lhs.as_ref() { + Expr::Identifier(_, _) => all_dots(dot_rhs), + _ => (false, dot_lhs.position()), }, - // variable.prop.prop.prop... - Expr::Dot(lhs, rhs, _) => match lhs.as_ref() { - // variable.prop - &Expr::Identifier(_, pos) => { - let r = all_identifiers(rhs); - (r.0, if r.0 { pos } else { r.1 }) - } - // all other property access is invalid - _ => (false, lhs.position()), - }, - // everything else is invalid _ => (false, expr.position()), } } - let r = all_identifiers(&lhs); - - if r.0 { - Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)) - } else { - Err(ParseError::new(PERR::AssignmentToInvalidLHS, r.1)) + match &lhs { + Expr::Identifier(_, _) => return Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)), + Expr::Index(idx_lhs, _, _) => match idx_lhs.as_ref() { + Expr::Identifier(_, _) => { + return Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)) + } + _ => (), + }, + Expr::Dot(_, _, _) => match all_dots(&lhs) { + (true, _) => return Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)), + (false, pos) => return Err(ParseError::new(PERR::AssignmentToInvalidLHS, pos)), + }, + _ => (), } + + return Err(ParseError::new( + PERR::AssignmentToInvalidLHS, + lhs.position(), + )); } fn parse_op_assignment( diff --git a/src/result.rs b/src/result.rs index 1d869c8b..8fcae55a 100644 --- a/src/result.rs +++ b/src/result.rs @@ -26,7 +26,7 @@ pub enum EvalAltResult { /// Wrapped values are the current number of characters in the string and the index number. ErrorStringBounds(usize, i64, Position), /// Trying to index into a type that is not an array and not a string. - ErrorIndexingType(Position), + ErrorIndexingType(String, Position), /// Trying to index into an array or string with an index that is not `i64`. ErrorIndexExpr(Position), /// The guard expression in an `if` statement does not return a boolean value. @@ -43,7 +43,7 @@ pub enum EvalAltResult { /// Error reading from a script file. Wrapped value is the path of the script file. ErrorReadingScriptFile(String, std::io::Error), /// Inappropriate member access. - ErrorDotExpr(Position), + ErrorDotExpr(String, Position), /// Arithmetic error encountered. Wrapped value is the error message. ErrorArithmetic(String, Position), /// Run-time error encountered. Wrapped value is the error message. @@ -65,7 +65,9 @@ impl Error for EvalAltResult { } Self::ErrorBooleanArgMismatch(_, _) => "Boolean operator expects boolean operands", Self::ErrorIndexExpr(_) => "Indexing into an array or string expects an integer index", - Self::ErrorIndexingType(_) => "Indexing can only be performed on an array or a string", + Self::ErrorIndexingType(_, _) => { + "Indexing can only be performed on an array or a string" + } Self::ErrorArrayBounds(_, index, _) if *index < 0 => { "Array access expects non-negative index" } @@ -84,7 +86,7 @@ impl Error for EvalAltResult { } Self::ErrorMismatchOutputType(_, _) => "Output type is incorrect", Self::ErrorReadingScriptFile(_, _) => "Cannot read from script file", - Self::ErrorDotExpr(_) => "Malformed dot expression", + Self::ErrorDotExpr(_, _) => "Malformed dot expression", Self::ErrorArithmetic(_, _) => "Arithmetic error", Self::ErrorRuntime(_, _) => "Runtime error", Self::LoopBreak => "[Not Error] Breaks out of loop", @@ -104,13 +106,14 @@ impl std::fmt::Display for EvalAltResult { match self { Self::ErrorFunctionNotFound(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos), Self::ErrorVariableNotFound(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos), - Self::ErrorIndexingType(pos) => write!(f, "{} ({})", desc, pos), + Self::ErrorIndexingType(_, pos) => write!(f, "{} ({})", desc, pos), Self::ErrorIndexExpr(pos) => write!(f, "{} ({})", desc, pos), Self::ErrorIfGuard(pos) => write!(f, "{} ({})", desc, pos), Self::ErrorFor(pos) => write!(f, "{} ({})", desc, pos), Self::ErrorAssignmentToUnknownLHS(pos) => write!(f, "{} ({})", desc, pos), Self::ErrorMismatchOutputType(s, pos) => write!(f, "{}: {} ({})", desc, s, pos), - Self::ErrorDotExpr(pos) => write!(f, "{} ({})", desc, pos), + Self::ErrorDotExpr(s, pos) if !s.is_empty() => 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), From df6950f8f776d374574ca8045db6c09b77c2e18b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 7 Mar 2020 20:55:03 +0800 Subject: [PATCH 08/27] Fix arrayindexed property access. --- src/engine.rs | 18 +++++++++++++++--- src/parser.rs | 17 ++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index bacd8982..b505950c 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -155,10 +155,22 @@ impl Engine<'_> { } else if let Some(val) = def_value { // Return default value Ok(val.clone()) - } else if spec.name.starts_with(FUNC_GETTER) || spec.name.starts_with(FUNC_SETTER) { - // Getter or setter + } else if spec.name.starts_with(FUNC_GETTER) { + // Getter Err(EvalAltResult::ErrorDotExpr( - "- invalid property access".to_string(), + format!( + "- unknown property '{}' or it is write-only", + &spec.name[FUNC_GETTER.len()..] + ), + pos, + )) + } else if spec.name.starts_with(FUNC_SETTER) { + // Setter + Err(EvalAltResult::ErrorDotExpr( + format!( + "- unknown property '{}' or it is read-only", + &spec.name[FUNC_SETTER.len()..] + ), pos, )) } else { diff --git a/src/parser.rs b/src/parser.rs index 5989b601..e14d081e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1275,6 +1275,7 @@ fn parse_unary<'a>(input: &mut Peekable>) -> Result Result { + //println!("{:?} = {:?}", lhs, rhs); fn all_dots(expr: &Expr) -> (bool, Position) { match expr { Expr::Identifier(_, pos) => (true, *pos), @@ -1294,9 +1295,19 @@ fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result (), }, - Expr::Dot(_, _, _) => match all_dots(&lhs) { - (true, _) => return Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)), - (false, pos) => return Err(ParseError::new(PERR::AssignmentToInvalidLHS, pos)), + Expr::Dot(dot_lhs, dot_rhs, _) => match dot_lhs.as_ref() { + Expr::Identifier(_, _) => match all_dots(&lhs) { + (true, _) => return Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)), + (false, pos) => return Err(ParseError::new(PERR::AssignmentToInvalidLHS, pos)), + }, + Expr::Index(idx_lhs, _, _) => match idx_lhs.as_ref() { + Expr::Identifier(_, _) => match all_dots(&dot_rhs) { + (true, _) => return Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)), + (false, pos) => return Err(ParseError::new(PERR::AssignmentToInvalidLHS, pos)), + }, + _ => (), + }, + _ => (), }, _ => (), } From eed7bef974ab596d6595fdc59846aad6137fe8fd Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 7 Mar 2020 22:50:46 +0800 Subject: [PATCH 09/27] Allow chaining of indexing (one level) and dotting. --- src/engine.rs | 197 +++++++++++++++++++++++++++++++++++++++----------- src/parser.rs | 49 +++++-------- src/result.rs | 6 ++ 3 files changed, 178 insertions(+), 74 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index b505950c..a8ec5ff5 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -220,8 +220,6 @@ impl Engine<'_> { // xxx.lhs[idx_expr] Expr::Index(lhs, idx_expr, idx_pos) => { - let idx = self.eval_index_value(scope, idx_expr)?; - let (lhs_value, _) = match lhs.as_ref() { Expr::Identifier(id, pos) => { let get_fn_name = format!("{}{}", FUNC_GETTER, id); @@ -235,6 +233,7 @@ impl Engine<'_> { } }; + let idx = self.eval_index_value(scope, idx_expr)?; self.get_indexed_value(lhs_value, idx, idx_expr.position(), *idx_pos) .map(|(v, _)| v) } @@ -250,8 +249,6 @@ impl Engine<'_> { } // xxx.lhs[idx_expr].rhs Expr::Index(lhs, idx_expr, idx_pos) => { - let idx = self.eval_index_value(scope, idx_expr)?; - let (lhs_value, _) = match lhs.as_ref() { Expr::Identifier(id, pos) => { let get_fn_name = format!("{}{}", FUNC_GETTER, id); @@ -268,6 +265,7 @@ impl Engine<'_> { } }; + let idx = self.eval_index_value(scope, idx_expr)?; self.get_indexed_value(lhs_value, idx, idx_expr.position(), *idx_pos) .and_then(|(mut value, _)| { self.get_dot_val_helper(scope, value.as_mut(), rhs) @@ -403,20 +401,20 @@ impl Engine<'_> { } /// Update the value at an index position in a variable inside the scope - fn update_indexed_variable_in_scope( + fn update_indexed_var_in_scope( src_type: IndexSourceType, scope: &mut Scope, id: &str, src_idx: usize, idx: usize, val: Dynamic, - ) -> Dynamic { + val_pos: Position, + ) -> Result { match src_type { // array_id[idx] = val IndexSourceType::Array => { let arr = scope.get_mut_by_type::(id, src_idx); - arr[idx as usize] = val; - ().into_dynamic() + Ok((arr[idx as usize] = val).into_dynamic()) } // string_id[idx] = val @@ -425,9 +423,8 @@ impl Engine<'_> { // Value must be a character let ch = *val .downcast::() - .expect("char value expected to update an index position in a string"); - Self::str_replace_char(s, idx as usize, ch); - ().into_dynamic() + .map_err(|_| EvalAltResult::ErrorCharMismatch(val_pos))?; + Ok(Self::str_replace_char(s, idx as usize, ch).into_dynamic()) } // All other variable types should be an error @@ -435,6 +432,31 @@ impl Engine<'_> { } } + /// Update the value at an index position + fn update_indexed_value( + mut val: Dynamic, + idx: usize, + new_val: Dynamic, + pos: Position, + ) -> Result { + if val.is::() { + let arr = val.downcast_mut::().expect("array expected"); + arr[idx as usize] = new_val; + } else if val.is::() { + let s = val.downcast_mut::().expect("string expected"); + // Value must be a character + let ch = *new_val + .downcast::() + .map_err(|_| EvalAltResult::ErrorCharMismatch(pos))?; + Self::str_replace_char(s, idx as usize, ch); + } else { + // All other variable types should be an error + panic!("array or string source type expected for indexing") + } + + Ok(val) + } + /// Evaluate a dot chain getter fn get_dot_val( &mut self, @@ -443,13 +465,12 @@ impl Engine<'_> { dot_rhs: &Expr, ) -> Result { match dot_lhs { - // xxx.??? + // id.??? Expr::Identifier(id, pos) => { let (src_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?; let value = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); - // In case the expression mutated `target`, we need to reassign it because - // of the above `clone`. + // In case the expression mutated `target`, we need to reassign it because of the above `clone`. *scope.get_mut(id, src_idx) = target; value @@ -461,12 +482,17 @@ impl Engine<'_> { self.eval_index_expr(scope, lhs, idx_expr, *idx_pos)?; let value = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); - // In case the expression mutated `target`, we need to reassign it because - // of the above `clone`. + // In case the expression mutated `target`, we need to reassign it because of the above `clone`. if let Some((id, src_idx)) = src { - Self::update_indexed_variable_in_scope( - src_type, scope, id, src_idx, idx, target, - ); + Self::update_indexed_var_in_scope( + src_type, + scope, + id, + src_idx, + idx, + target, + lhs.position(), + )?; } value @@ -483,31 +509,54 @@ impl Engine<'_> { /// Chain-evaluate a dot setter fn set_dot_val_helper( &mut self, + scope: &mut Scope, this_ptr: &mut Variant, dot_rhs: &Expr, - mut source_val: Dynamic, + mut new_val: Dynamic, + val_pos: Position, ) -> Result { match dot_rhs { // xxx.id Expr::Identifier(id, pos) => { let set_fn_name = format!("{}{}", FUNC_SETTER, id); - self.call_fn_raw( - &set_fn_name, - vec![this_ptr, source_val.as_mut()], - None, - *pos, - ) + self.call_fn_raw(&set_fn_name, vec![this_ptr, new_val.as_mut()], None, *pos) } - // xxx.lhs.rhs + // xxx.lhs[idx_expr] + // TODO - Allow chaining of indexing! + Expr::Index(lhs, idx_expr, idx_pos) => match lhs.as_ref() { + // xxx.id[idx_expr] + Expr::Identifier(id, pos) => { + let get_fn_name = format!("{}{}", FUNC_GETTER, id); + + self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) + .and_then(|v| { + let idx = self.eval_index_value(scope, idx_expr)?; + Self::update_indexed_value(v, idx as usize, new_val, val_pos) + }) + .and_then(|mut v| { + let set_fn_name = format!("{}{}", FUNC_SETTER, id); + self.call_fn_raw(&set_fn_name, vec![this_ptr, v.as_mut()], None, *pos) + }) + } + + // All others - syntax error for setters chain + _ => Err(EvalAltResult::ErrorDotExpr( + "for assignment".to_string(), + *idx_pos, + )), + }, + + // xxx.lhs.{...} Expr::Dot(lhs, rhs, _) => match lhs.as_ref() { + // xxx.id.rhs Expr::Identifier(id, pos) => { let get_fn_name = format!("{}{}", FUNC_GETTER, id); self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) .and_then(|mut v| { - self.set_dot_val_helper(v.as_mut(), rhs, source_val) + self.set_dot_val_helper(scope, v.as_mut(), rhs, new_val, val_pos) .map(|_| v) // Discard Ok return value }) .and_then(|mut v| { @@ -516,6 +565,55 @@ impl Engine<'_> { self.call_fn_raw(&set_fn_name, vec![this_ptr, v.as_mut()], None, *pos) }) } + + // xxx.lhs[idx_expr].rhs + // TODO - Allow chaining of indexing! + Expr::Index(lhs, idx_expr, idx_pos) => match lhs.as_ref() { + // xxx.id[idx_expr].rhs + Expr::Identifier(id, pos) => { + let get_fn_name = format!("{}{}", FUNC_GETTER, id); + + self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) + .and_then(|v| { + let idx = self.eval_index_value(scope, idx_expr)?; + let (mut target, _) = self.get_indexed_value( + v.clone(), // TODO - Avoid cloning this + idx, + idx_expr.position(), + *idx_pos, + )?; + + self.set_dot_val_helper( + scope, + target.as_mut(), + rhs, + new_val, + val_pos, + )?; + + // In case the expression mutated `target`, we need to reassign it because of the above `clone`. + Self::update_indexed_value(v, idx as usize, target, val_pos) + }) + .and_then(|mut v| { + let set_fn_name = format!("{}{}", FUNC_SETTER, id); + + self.call_fn_raw( + &set_fn_name, + vec![this_ptr, v.as_mut()], + None, + *pos, + ) + }) + } + + // All others - syntax error for setters chain + _ => Err(EvalAltResult::ErrorDotExpr( + "for assignment".to_string(), + *idx_pos, + )), + }, + + // All others - syntax error for setters chain _ => Err(EvalAltResult::ErrorDotExpr( "for assignment".to_string(), lhs.position(), @@ -536,34 +634,41 @@ impl Engine<'_> { scope: &mut Scope, dot_lhs: &Expr, dot_rhs: &Expr, - source_val: Dynamic, + new_val: Dynamic, + val_pos: Position, ) -> Result { match dot_lhs { // id.??? Expr::Identifier(id, pos) => { let (src_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?; - let value = self.set_dot_val_helper(target.as_mut(), dot_rhs, source_val); + let value = + self.set_dot_val_helper(scope, target.as_mut(), dot_rhs, new_val, val_pos); - // In case the expression mutated `target`, we need to reassign it because - // of the above `clone`. + // In case the expression mutated `target`, we need to reassign it because of the above `clone`. *scope.get_mut(id, src_idx) = target; value } // lhs[idx_expr].??? + // TODO - Allow chaining of indexing! Expr::Index(lhs, idx_expr, idx_pos) => { let (src_type, src, idx, mut target) = self.eval_index_expr(scope, lhs, idx_expr, *idx_pos)?; - let value = self.set_dot_val_helper(target.as_mut(), dot_rhs, source_val); - - // In case the expression mutated `target`, we need to reassign it because - // of the above `clone`. + let value = + self.set_dot_val_helper(scope, target.as_mut(), dot_rhs, new_val, val_pos); + // In case the expression mutated `target`, we need to reassign it because of the above `clone`. if let Some((id, src_idx)) = src { - Self::update_indexed_variable_in_scope( - src_type, scope, id, src_idx, idx, target, - ); + Self::update_indexed_var_in_scope( + src_type, + scope, + id, + src_idx, + idx, + target, + lhs.position(), + )?; } value @@ -617,9 +722,15 @@ impl Engine<'_> { self.eval_index_expr(scope, idx_lhs, idx_expr, *idx_pos)?; if let Some((id, src_idx)) = src { - Ok(Self::update_indexed_variable_in_scope( - src_type, scope, &id, src_idx, idx, rhs_val, - )) + Ok(Self::update_indexed_var_in_scope( + src_type, + scope, + &id, + src_idx, + idx, + rhs_val, + rhs.position(), + )?) } else { Err(EvalAltResult::ErrorAssignmentToUnknownLHS( idx_lhs.position(), @@ -629,7 +740,7 @@ impl Engine<'_> { // dot_lhs.dot_rhs = rhs Expr::Dot(dot_lhs, dot_rhs, _) => { - self.set_dot_val(scope, dot_lhs, dot_rhs, rhs_val) + self.set_dot_val(scope, dot_lhs, dot_rhs, rhs_val, rhs.position()) } // Syntax error diff --git a/src/parser.rs b/src/parser.rs index e14d081e..a395d21e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1275,47 +1275,34 @@ fn parse_unary<'a>(input: &mut Peekable>) -> Result Result { - //println!("{:?} = {:?}", lhs, rhs); - fn all_dots(expr: &Expr) -> (bool, Position) { + fn valid_assignment_chain(expr: &Expr) -> (bool, Position) { match expr { Expr::Identifier(_, pos) => (true, *pos), + + Expr::Index(idx_lhs, _, _) => match idx_lhs.as_ref() { + Expr::Identifier(_, _) => (true, idx_lhs.position()), + _ => (false, idx_lhs.position()), + }, + Expr::Dot(dot_lhs, dot_rhs, _) => match dot_lhs.as_ref() { - Expr::Identifier(_, _) => all_dots(dot_rhs), + Expr::Identifier(_, _) => valid_assignment_chain(dot_rhs), + Expr::Index(idx_lhs, _, _) => match idx_lhs.as_ref() { + Expr::Identifier(_, _) => valid_assignment_chain(dot_rhs), + _ => (false, idx_lhs.position()), + }, _ => (false, dot_lhs.position()), }, + _ => (false, expr.position()), } } - match &lhs { - Expr::Identifier(_, _) => return Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)), - Expr::Index(idx_lhs, _, _) => match idx_lhs.as_ref() { - Expr::Identifier(_, _) => { - return Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)) - } - _ => (), - }, - Expr::Dot(dot_lhs, dot_rhs, _) => match dot_lhs.as_ref() { - Expr::Identifier(_, _) => match all_dots(&lhs) { - (true, _) => return Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)), - (false, pos) => return Err(ParseError::new(PERR::AssignmentToInvalidLHS, pos)), - }, - Expr::Index(idx_lhs, _, _) => match idx_lhs.as_ref() { - Expr::Identifier(_, _) => match all_dots(&dot_rhs) { - (true, _) => return Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)), - (false, pos) => return Err(ParseError::new(PERR::AssignmentToInvalidLHS, pos)), - }, - _ => (), - }, - _ => (), - }, - _ => (), - } + //println!("{:?} = {:?}", lhs, rhs); - return Err(ParseError::new( - PERR::AssignmentToInvalidLHS, - lhs.position(), - )); + match valid_assignment_chain(&lhs) { + (true, _) => Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)), + (false, pos) => Err(ParseError::new(PERR::AssignmentToInvalidLHS, pos)), + } } fn parse_op_assignment( diff --git a/src/result.rs b/src/result.rs index 8fcae55a..d9e009d2 100644 --- a/src/result.rs +++ b/src/result.rs @@ -19,6 +19,8 @@ pub enum EvalAltResult { ErrorFunctionArgsMismatch(String, usize, usize, Position), /// Non-boolean operand encountered for boolean operator. Wrapped value is the operator. ErrorBooleanArgMismatch(String, Position), + /// Non-character value encountered where a character is required. + ErrorCharMismatch(Position), /// Array access out-of-bounds. /// Wrapped values are the current number of elements in the array and the index number. ErrorArrayBounds(usize, i64, Position), @@ -64,6 +66,7 @@ impl Error for EvalAltResult { "Function call with wrong number of arguments" } Self::ErrorBooleanArgMismatch(_, _) => "Boolean operator expects boolean operands", + Self::ErrorCharMismatch(_) => "Character expected", Self::ErrorIndexExpr(_) => "Indexing into an array or string expects an integer index", Self::ErrorIndexingType(_, _) => { "Indexing can only be performed on an array or a string" @@ -131,6 +134,9 @@ impl std::fmt::Display for EvalAltResult { Self::ErrorBooleanArgMismatch(op, pos) => { write!(f, "{} operator expects boolean operands ({})", op, pos) } + Self::ErrorCharMismatch(pos) => { + write!(f, "string indexing expects a character value ({})", pos) + } Self::ErrorArrayBounds(_, index, pos) if *index < 0 => { write!(f, "{}: {} < 0 ({})", desc, index, pos) } From a264abffa442a010644d4e16fc6489c20f5109c5 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 7 Mar 2020 23:57:01 +0800 Subject: [PATCH 10/27] Allow arbitrary number of indexing in dot getter chain. --- src/engine.rs | 142 ++++++++++++++++++++++++++++---------------------- 1 file changed, 80 insertions(+), 62 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index a8ec5ff5..5203c495 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -218,9 +218,10 @@ impl Engine<'_> { self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) } - // xxx.lhs[idx_expr] - Expr::Index(lhs, idx_expr, idx_pos) => { - let (lhs_value, _) = match lhs.as_ref() { + // xxx.idx_lhs[idx_expr] + Expr::Index(idx_lhs, idx_expr, idx_pos) => { + let (lhs_value, _) = match idx_lhs.as_ref() { + // xxx.id[idx_expr] Expr::Identifier(id, pos) => { let get_fn_name = format!("{}{}", FUNC_GETTER, id); ( @@ -228,8 +229,16 @@ impl Engine<'_> { *pos, ) } - expr => { - return Err(EvalAltResult::ErrorDotExpr("".to_string(), expr.position())) + // xxx.???[???][idx_expr] + Expr::Index(_, _, _) => { + (self.get_dot_val_helper(scope, this_ptr, idx_lhs)?, *idx_pos) + } + // Syntax error + _ => { + return Err(EvalAltResult::ErrorDotExpr( + "".to_string(), + dot_rhs.position(), + )) } }; @@ -238,8 +247,8 @@ impl Engine<'_> { .map(|(v, _)| v) } - // xxx.lhs.rhs - Expr::Dot(lhs, rhs, _) => match lhs.as_ref() { + // xxx.dot_lhs.rhs + Expr::Dot(dot_lhs, rhs, _) => match dot_lhs.as_ref() { // xxx.id.rhs Expr::Identifier(id, pos) => { let get_fn_name = format!("{}{}", FUNC_GETTER, id); @@ -247,9 +256,10 @@ impl Engine<'_> { self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) .and_then(|mut v| self.get_dot_val_helper(scope, v.as_mut(), rhs)) } - // xxx.lhs[idx_expr].rhs - Expr::Index(lhs, idx_expr, idx_pos) => { - let (lhs_value, _) = match lhs.as_ref() { + // xxx.idx_lhs[idx_expr].rhs + Expr::Index(idx_lhs, idx_expr, idx_pos) => { + let (lhs_value, _) = match idx_lhs.as_ref() { + // xxx.id[idx_expr].rhs Expr::Identifier(id, pos) => { let get_fn_name = format!("{}{}", FUNC_GETTER, id); ( @@ -257,10 +267,15 @@ impl Engine<'_> { *pos, ) } - expr => { + // xxx.???[???][idx_expr].rhs + Expr::Index(_, _, _) => { + (self.get_dot_val_helper(scope, this_ptr, idx_lhs)?, *idx_pos) + } + // Syntax error + _ => { return Err(EvalAltResult::ErrorDotExpr( "".to_string(), - expr.position(), + dot_rhs.position(), )) } }; @@ -272,7 +287,10 @@ impl Engine<'_> { }) } // Syntax error - _ => Err(EvalAltResult::ErrorDotExpr("".to_string(), lhs.position())), + _ => Err(EvalAltResult::ErrorDotExpr( + "".to_string(), + dot_lhs.position(), + )), }, // Syntax error @@ -283,6 +301,55 @@ impl Engine<'_> { } } + /// Evaluate a dot chain getter + fn get_dot_val( + &mut self, + scope: &mut Scope, + dot_lhs: &Expr, + dot_rhs: &Expr, + ) -> Result { + match dot_lhs { + // id.??? + Expr::Identifier(id, pos) => { + let (src_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?; + let value = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); + + // In case the expression mutated `target`, we need to reassign it because of the above `clone`. + *scope.get_mut(id, src_idx) = target; + + value + } + + // idx_lhs[idx_expr].??? + Expr::Index(idx_lhs, idx_expr, idx_pos) => { + let (src_type, src, idx, mut target) = + self.eval_index_expr(scope, idx_lhs, idx_expr, *idx_pos)?; + let value = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); + + // In case the expression mutated `target`, we need to reassign it because of the above `clone`. + if let Some((id, src_idx)) = src { + Self::update_indexed_var_in_scope( + src_type, + scope, + id, + src_idx, + idx, + target, + idx_lhs.position(), + )?; + } + + value + } + + // {expr}.??? + expr => { + let mut target = self.eval_expr(scope, expr)?; + self.get_dot_val_helper(scope, target.as_mut(), dot_rhs) + } + } + } + /// Search for a variable within the scope, returning its value and index inside the Scope fn search_scope( scope: &Scope, @@ -457,55 +524,6 @@ impl Engine<'_> { Ok(val) } - /// Evaluate a dot chain getter - fn get_dot_val( - &mut self, - scope: &mut Scope, - dot_lhs: &Expr, - dot_rhs: &Expr, - ) -> Result { - match dot_lhs { - // id.??? - Expr::Identifier(id, pos) => { - let (src_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?; - let value = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); - - // In case the expression mutated `target`, we need to reassign it because of the above `clone`. - *scope.get_mut(id, src_idx) = target; - - value - } - - // lhs[idx_expr].??? - Expr::Index(lhs, idx_expr, idx_pos) => { - let (src_type, src, idx, mut target) = - self.eval_index_expr(scope, lhs, idx_expr, *idx_pos)?; - let value = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); - - // In case the expression mutated `target`, we need to reassign it because of the above `clone`. - if let Some((id, src_idx)) = src { - Self::update_indexed_var_in_scope( - src_type, - scope, - id, - src_idx, - idx, - target, - lhs.position(), - )?; - } - - value - } - - // {expr}.??? - expr => { - let mut target = self.eval_expr(scope, expr)?; - self.get_dot_val_helper(scope, target.as_mut(), dot_rhs) - } - } - } - /// Chain-evaluate a dot setter fn set_dot_val_helper( &mut self, From daa581bac7f290cbce598c840bc687e26e0d2dcb Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 8 Mar 2020 09:19:04 +0800 Subject: [PATCH 11/27] Add `append` to strings. --- README.md | 1 + src/builtin.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/README.md b/README.md index a379b829..1c7a3223 100644 --- a/README.md +++ b/README.md @@ -777,6 +777,7 @@ The following standard functions (defined in the standard library but excluded i * `len` - returns the number of characters (not number of bytes) in the string * `pad` - pads the string with an character until a specified number of characters +* `append` - Adds a character or a string to the end of another string * `clear` - empties the string * `truncate` - cuts off the string at exactly a specified number of characters * `contains` - checks if a certain character or sub-string occurs in the string diff --git a/src/builtin.rs b/src/builtin.rs index ba175ea0..7c43f0a4 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -303,6 +303,8 @@ impl Engine<'_> { self.register_fn("contains", |s: &mut String, ch: char| s.contains(ch)); self.register_fn("contains", |s: &mut String, find: String| s.contains(&find)); self.register_fn("clear", |s: &mut String| s.clear()); + self.register_fn("append", |s: &mut String, ch: char| s.push(ch)); + self.register_fn("append", |s: &mut String, add: String| s.push_str(&add)); self.register_fn("truncate", |s: &mut String, len: i64| { if len >= 0 { let chars: Vec<_> = s.chars().take(len as usize).collect(); From 3e7adc2e514cef6ece94fb019a01171ca478e6b4 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 8 Mar 2020 19:54:02 +0800 Subject: [PATCH 12/27] More comments in code. --- src/any.rs | 2 + src/api.rs | 27 +++++----- src/builtin.rs | 3 ++ src/call.rs | 3 +- src/engine.rs | 125 ++++++++++++++++++++++++--------------------- src/error.rs | 6 +-- src/fn_register.rs | 3 +- src/parser.rs | 14 ++--- src/result.rs | 7 +-- src/scope.rs | 2 + 10 files changed, 106 insertions(+), 86 deletions(-) diff --git a/src/any.rs b/src/any.rs index c53c8c74..c2003da4 100644 --- a/src/any.rs +++ b/src/any.rs @@ -1,3 +1,5 @@ +//! Helper module which defines the `Any` trait to to allow dynamic value handling. + use std::any::{type_name, TypeId}; use std::fmt; diff --git a/src/api.rs b/src/api.rs index d38dad9a..698affa7 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,3 +1,5 @@ +//! Module that defines the extern API of `Engine`. + use crate::any::{Any, AnyExt, Dynamic}; use crate::call::FuncArgs; use crate::engine::{Engine, FnAny, FnIntExt, FnSpec}; @@ -6,7 +8,7 @@ use crate::fn_register::RegisterFn; use crate::parser::{lex, parse, Position, AST}; use crate::result::EvalAltResult; use crate::scope::Scope; -use std::any::TypeId; +use std::any::{type_name, TypeId}; use std::sync::Arc; impl<'e> Engine<'e> { @@ -17,12 +19,16 @@ impl<'e> Engine<'e> { f: Box, ) { debug_println!( - "Register function: {} for {} parameter(s)", + "Register function: {} with {}", fn_name, if let Some(a) = &args { - format!("{}", a.len()) + format!( + "{} parameter{}", + a.len(), + if a.len() > 1 { "s" } else { "" } + ) } else { - "no".to_string() + "no parameter".to_string() } ); @@ -31,24 +37,21 @@ impl<'e> Engine<'e> { args, }; - self.external_functions - .insert(spec, Arc::new(FnIntExt::Ext(f))); + self.ext_functions.insert(spec, Arc::new(FnIntExt::Ext(f))); } /// Register a custom type for use with the `Engine`. /// The type must be `Clone`. pub fn register_type(&mut self) { - self.register_type_with_name::(std::any::type_name::()); + self.register_type_with_name::(type_name::()); } /// Register a custom type for use with the `Engine` with a name for the `type_of` function. /// The type must be `Clone`. - pub fn register_type_with_name(&mut self, type_name: &str) { + pub fn register_type_with_name(&mut self, name: &str) { // Add the pretty-print type name into the map - self.type_names.insert( - std::any::type_name::().to_string(), - type_name.to_string(), - ); + self.type_names + .insert(type_name::().to_string(), name.to_string()); } /// Register an iterator adapter for a type with the `Engine`. diff --git a/src/builtin.rs b/src/builtin.rs index 7c43f0a4..66789ef3 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -1,3 +1,6 @@ +//! Helper module that allows registration of the _core library_ and +//! _standard library_ of utility functions. + use crate::any::Any; use crate::engine::{Array, Engine}; use crate::fn_register::RegisterFn; diff --git a/src/call.rs b/src/call.rs index b287f2bf..065cc8d5 100644 --- a/src/call.rs +++ b/src/call.rs @@ -1,5 +1,4 @@ -//! Helper module which defines `FnArgs` -//! to make function calling easier. +//! Helper module which defines `FnArgs` to make function calling easier. use crate::any::{Any, Variant}; diff --git a/src/engine.rs b/src/engine.rs index 5203c495..1c13637e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,19 +1,25 @@ -use std::any::TypeId; -use std::borrow::Cow; -use std::cmp::{PartialEq, PartialOrd}; -use std::collections::HashMap; -use std::sync::Arc; +//! Main module defining the script evaluation `Engine`. use crate::any::{Any, AnyExt, Dynamic, Variant}; use crate::parser::{Expr, FnDef, Position, Stmt}; use crate::result::EvalAltResult; use crate::scope::Scope; +use std::any::TypeId; +use std::borrow::Cow; +use std::cmp::{PartialEq, PartialOrd}; +use std::collections::HashMap; +use std::iter::once; +use std::sync::Arc; /// An dynamic array of `Dynamic` values. pub type Array = Vec; pub type FnCallArgs<'a> = Vec<&'a mut Variant>; +pub type FnAny = dyn Fn(FnCallArgs, Position) -> Result; + +type IteratorFn = dyn Fn(&Dynamic) -> Box>; + const KEYWORD_PRINT: &'static str = "print"; const KEYWORD_DEBUG: &'static str = "debug"; const KEYWORD_TYPE_OF: &'static str = "type_of"; @@ -33,8 +39,6 @@ pub struct FnSpec<'a> { pub args: Option>, } -type IteratorFn = dyn Fn(&Dynamic) -> Box>; - /// Rhai main scripting engine. /// /// ```rust @@ -50,13 +54,14 @@ type IteratorFn = dyn Fn(&Dynamic) -> Box>; /// ``` pub struct Engine<'e> { /// A hashmap containing all compiled functions known to the engine - pub(crate) external_functions: HashMap, Arc>>, + pub(crate) ext_functions: HashMap, Arc>>, /// A hashmap containing all script-defined functions pub(crate) script_functions: HashMap, Arc>>, /// A hashmap containing all iterators known to the engine pub(crate) type_iterators: HashMap>, pub(crate) type_names: HashMap, + // Closures for implementing the print/debug commands pub(crate) on_print: Box, pub(crate) on_debug: Box, } @@ -66,8 +71,6 @@ pub enum FnIntExt<'a> { Int(FnDef<'a>), } -pub type FnAny = dyn Fn(FnCallArgs, Position) -> Result; - impl Engine<'_> { /// Universal method for calling functions, that are either /// registered with the `Engine` or written in Rhai @@ -75,7 +78,7 @@ impl Engine<'_> { &mut self, fn_name: &str, args: FnCallArgs, - def_value: Option<&Dynamic>, + def_val: Option<&Dynamic>, pos: Position, ) -> Result { debug_println!( @@ -94,21 +97,23 @@ impl Engine<'_> { }; // First search in script-defined functions (can override built-in), - // then in built-in's + // then built-in's and external functions let fn_def = self .script_functions .get(&spec) .or_else(|| { spec.args = Some(args.iter().map(|a| Any::type_id(&**a)).collect()); - self.external_functions.get(&spec) + self.ext_functions.get(&spec) }) .map(|f| f.clone()); if let Some(f) = fn_def { match *f { + // Run external function FnIntExt::Ext(ref func) => { let result = func(args, pos)?; + // See if the function match print/debug (which requires special processing) let callback = match spec.name.as_ref() { KEYWORD_PRINT => self.on_print.as_mut(), KEYWORD_DEBUG => self.on_debug.as_mut(), @@ -122,7 +127,10 @@ impl Engine<'_> { Ok(callback(val).into_dynamic()) } + + // Run script-defined function FnIntExt::Int(ref func) => { + // First check number of parameters if func.params.len() != args.len() { return Err(EvalAltResult::ErrorFunctionArgsMismatch( spec.name.into(), @@ -135,45 +143,50 @@ impl Engine<'_> { let mut scope = Scope::new(); scope.extend( + // Put arguments into scope as variables func.params .iter() - .map(|s| s.clone()) + .cloned() .zip(args.iter().map(|x| (*x).into_dynamic())), ); + // Evaluate match self.eval_stmt(&mut scope, &*func.body) { + // Convert return statement to return value Err(EvalAltResult::Return(x, _)) => Ok(x), other => other, } } } } else if spec.name == KEYWORD_TYPE_OF && args.len() == 1 { + // Handle `type_of` function Ok(self .map_type_name(args[0].type_name()) .to_string() .into_dynamic()) - } else if let Some(val) = def_value { - // Return default value - Ok(val.clone()) } else if spec.name.starts_with(FUNC_GETTER) { - // Getter + // Getter function not found Err(EvalAltResult::ErrorDotExpr( format!( - "- unknown property '{}' or it is write-only", + "- property '{}' unknown or write-only", &spec.name[FUNC_GETTER.len()..] ), pos, )) } else if spec.name.starts_with(FUNC_SETTER) { - // Setter + // Setter function not found Err(EvalAltResult::ErrorDotExpr( format!( - "- unknown property '{}' or it is read-only", + "- property '{}' unknown or read-only", &spec.name[FUNC_SETTER.len()..] ), pos, )) + } else if let Some(val) = def_val { + // Return default value + Ok(val.clone()) } else { + // Raise error let types_list = args .iter() .map(|x| (*x).type_name()) @@ -194,11 +207,9 @@ impl Engine<'_> { this_ptr: &mut Variant, dot_rhs: &Expr, ) -> Result { - use std::iter::once; - match dot_rhs { // xxx.fn_name(args) - Expr::FunctionCall(fn_name, args, def_value, pos) => { + Expr::FunctionCall(fn_name, args, def_val, pos) => { let mut args: Array = args .iter() .map(|arg| self.eval_expr(scope, arg)) @@ -208,7 +219,7 @@ impl Engine<'_> { .chain(args.iter_mut().map(|b| b.as_mut())) .collect(); - self.call_fn_raw(fn_name, args, def_value.as_ref(), *pos) + self.call_fn_raw(fn_name, args, def_val.as_ref(), *pos) } // xxx.id @@ -220,7 +231,7 @@ impl Engine<'_> { // xxx.idx_lhs[idx_expr] Expr::Index(idx_lhs, idx_expr, idx_pos) => { - let (lhs_value, _) = match idx_lhs.as_ref() { + let (expr, _) = match idx_lhs.as_ref() { // xxx.id[idx_expr] Expr::Identifier(id, pos) => { let get_fn_name = format!("{}{}", FUNC_GETTER, id); @@ -243,7 +254,7 @@ impl Engine<'_> { }; let idx = self.eval_index_value(scope, idx_expr)?; - self.get_indexed_value(lhs_value, idx, idx_expr.position(), *idx_pos) + self.get_indexed_value(expr, idx, idx_expr.position(), *idx_pos) .map(|(v, _)| v) } @@ -258,7 +269,7 @@ impl Engine<'_> { } // xxx.idx_lhs[idx_expr].rhs Expr::Index(idx_lhs, idx_expr, idx_pos) => { - let (lhs_value, _) = match idx_lhs.as_ref() { + let (expr, _) = match idx_lhs.as_ref() { // xxx.id[idx_expr].rhs Expr::Identifier(id, pos) => { let get_fn_name = format!("{}{}", FUNC_GETTER, id); @@ -281,10 +292,8 @@ impl Engine<'_> { }; let idx = self.eval_index_value(scope, idx_expr)?; - self.get_indexed_value(lhs_value, idx, idx_expr.position(), *idx_pos) - .and_then(|(mut value, _)| { - self.get_dot_val_helper(scope, value.as_mut(), rhs) - }) + self.get_indexed_value(expr, idx, idx_expr.position(), *idx_pos) + .and_then(|(mut v, _)| self.get_dot_val_helper(scope, v.as_mut(), rhs)) } // Syntax error _ => Err(EvalAltResult::ErrorDotExpr( @@ -312,21 +321,21 @@ impl Engine<'_> { // id.??? Expr::Identifier(id, pos) => { let (src_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?; - let value = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); + let val = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); - // In case the expression mutated `target`, we need to reassign it because of the above `clone`. + // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. *scope.get_mut(id, src_idx) = target; - value + val } // idx_lhs[idx_expr].??? Expr::Index(idx_lhs, idx_expr, idx_pos) => { let (src_type, src, idx, mut target) = self.eval_index_expr(scope, idx_lhs, idx_expr, *idx_pos)?; - let value = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); + let val = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); - // In case the expression mutated `target`, we need to reassign it because of the above `clone`. + // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. if let Some((id, src_idx)) = src { Self::update_indexed_var_in_scope( src_type, @@ -339,7 +348,7 @@ impl Engine<'_> { )?; } - value + val } // {expr}.??? @@ -449,7 +458,7 @@ impl Engine<'_> { let val = self.eval_expr(scope, expr)?; self.get_indexed_value(val, idx, idx_expr.position(), idx_pos) - .map(|(value, _)| (IndexSourceType::Expression, None, idx as usize, value)) + .map(|(v, _)| (IndexSourceType::Expression, None, idx as usize, v)) } } } @@ -474,21 +483,21 @@ impl Engine<'_> { id: &str, src_idx: usize, idx: usize, - val: Dynamic, + new_val: Dynamic, val_pos: Position, ) -> Result { match src_type { // array_id[idx] = val IndexSourceType::Array => { let arr = scope.get_mut_by_type::(id, src_idx); - Ok((arr[idx as usize] = val).into_dynamic()) + Ok((arr[idx as usize] = new_val).into_dynamic()) } // string_id[idx] = val IndexSourceType::String => { let s = scope.get_mut_by_type::(id, src_idx); // Value must be a character - let ch = *val + let ch = *new_val .downcast::() .map_err(|_| EvalAltResult::ErrorCharMismatch(val_pos))?; Ok(Self::str_replace_char(s, idx as usize, ch).into_dynamic()) @@ -501,16 +510,16 @@ impl Engine<'_> { /// Update the value at an index position fn update_indexed_value( - mut val: Dynamic, + mut target: Dynamic, idx: usize, new_val: Dynamic, pos: Position, ) -> Result { - if val.is::() { - let arr = val.downcast_mut::().expect("array expected"); + if target.is::() { + let arr = target.downcast_mut::().expect("array expected"); arr[idx as usize] = new_val; - } else if val.is::() { - let s = val.downcast_mut::().expect("string expected"); + } else if target.is::() { + let s = target.downcast_mut::().expect("string expected"); // Value must be a character let ch = *new_val .downcast::() @@ -521,7 +530,7 @@ impl Engine<'_> { panic!("array or string source type expected for indexing") } - Ok(val) + Ok(target) } /// Chain-evaluate a dot setter @@ -609,7 +618,7 @@ impl Engine<'_> { val_pos, )?; - // In case the expression mutated `target`, we need to reassign it because of the above `clone`. + // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. Self::update_indexed_value(v, idx as usize, target, val_pos) }) .and_then(|mut v| { @@ -659,13 +668,13 @@ impl Engine<'_> { // id.??? Expr::Identifier(id, pos) => { let (src_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?; - let value = + let val = self.set_dot_val_helper(scope, target.as_mut(), dot_rhs, new_val, val_pos); - // In case the expression mutated `target`, we need to reassign it because of the above `clone`. + // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. *scope.get_mut(id, src_idx) = target; - value + val } // lhs[idx_expr].??? @@ -673,10 +682,10 @@ impl Engine<'_> { Expr::Index(lhs, idx_expr, idx_pos) => { let (src_type, src, idx, mut target) = self.eval_index_expr(scope, lhs, idx_expr, *idx_pos)?; - let value = + let val = self.set_dot_val_helper(scope, target.as_mut(), dot_rhs, new_val, val_pos); - // In case the expression mutated `target`, we need to reassign it because of the above `clone`. + // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. if let Some((id, src_idx)) = src { Self::update_indexed_var_in_scope( src_type, @@ -689,7 +698,7 @@ impl Engine<'_> { )?; } - value + val } // Syntax error @@ -782,7 +791,7 @@ impl Engine<'_> { Ok(Box::new(arr)) } - Expr::FunctionCall(fn_name, args, def_value, pos) => { + Expr::FunctionCall(fn_name, args, def_val, pos) => { let mut args = args .iter() .map(|expr| self.eval_expr(scope, expr)) @@ -791,7 +800,7 @@ impl Engine<'_> { self.call_fn_raw( fn_name, args.iter_mut().map(|b| b.as_mut()).collect(), - def_value.as_ref(), + def_val.as_ref(), *pos, ) } @@ -996,7 +1005,7 @@ impl Engine<'_> { // Create the new scripting Engine let mut engine = Engine { - external_functions: HashMap::new(), + ext_functions: HashMap::new(), script_functions: HashMap::new(), type_iterators: HashMap::new(), type_names, diff --git a/src/error.rs b/src/error.rs index d4096cb1..41a793c3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,7 @@ +//! Module containing error definitions for the parsing process. + use crate::parser::Position; -use std::char; -use std::error::Error; -use std::fmt; +use std::{char, error::Error, fmt}; /// Error when tokenizing the script text. #[derive(Debug, Eq, PartialEq, Hash, Clone)] diff --git a/src/fn_register.rs b/src/fn_register.rs index 9e84492f..535ffc83 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -1,9 +1,10 @@ -use std::any::TypeId; +//! Module which defines the function registration mechanism. use crate::any::{Any, Dynamic}; use crate::engine::{Engine, FnCallArgs}; use crate::parser::Position; use crate::result::EvalAltResult; +use std::any::TypeId; /// A trait to register custom functions with the `Engine`. /// diff --git a/src/parser.rs b/src/parser.rs index a395d21e..ad714187 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,8 +1,8 @@ +//! Main module defining the lexer and parser. + use crate::any::Dynamic; use crate::error::{LexError, ParseError, ParseErrorType}; -use std::char; -use std::iter::Peekable; -use std::{borrow::Cow, str::Chars}; +use std::{borrow::Cow, char, fmt, iter::Peekable, str::Chars}; type LERR = LexError; type PERR = ParseErrorType; @@ -78,8 +78,8 @@ impl Default for Position { } } -impl std::fmt::Display for Position { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for Position { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.is_eof() { write!(f, "EOF") } else { @@ -88,8 +88,8 @@ impl std::fmt::Display for Position { } } -impl std::fmt::Debug for Position { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Debug for Position { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.is_eof() { write!(f, "(EOF)") } else { diff --git a/src/result.rs b/src/result.rs index d9e009d2..26272030 100644 --- a/src/result.rs +++ b/src/result.rs @@ -1,8 +1,9 @@ -use std::error::Error; +//! Module containing error definitions for the evaluation process. use crate::any::Dynamic; use crate::error::ParseError; use crate::parser::Position; +use std::{error::Error, fmt}; /// Evaluation result. /// @@ -102,8 +103,8 @@ impl Error for EvalAltResult { } } -impl std::fmt::Display for EvalAltResult { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for EvalAltResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let desc = self.description(); match self { diff --git a/src/scope.rs b/src/scope.rs index 9a5eb815..01bcebda 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,3 +1,5 @@ +//! Module that defines the `Scope` type representing a function call-stack scope. + use crate::any::{Any, Dynamic}; use std::borrow::Cow; From b1b25d3043e194ca6e74a40ba6c20aabe864d7a4 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 8 Mar 2020 22:47:13 +0800 Subject: [PATCH 13/27] Add fallible functions support and replace most arithmetic operations with checked versions. --- Cargo.toml | 3 + README.md | 40 +++++++++- src/builtin.rs | 187 ++++++++++++++++++++++++++++++++++++++++----- src/error.rs | 3 + src/fn_register.rs | 68 ++++++++++++++++- src/lib.rs | 2 +- src/parser.rs | 43 ++++++++--- src/result.rs | 41 +++++++++- tests/math.rs | 40 ++++++++++ 9 files changed, 387 insertions(+), 40 deletions(-) create mode 100644 tests/math.rs diff --git a/Cargo.toml b/Cargo.toml index f586d1f3..e39a0115 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,9 @@ include = [ "Cargo.toml" ] +[dependencies] +num-traits = "*" + [features] debug_msgs = [] no_stdlib = [] \ No newline at end of file diff --git a/README.md b/README.md index 1c7a3223..9eaacd38 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,9 @@ if z.type_of() == "string" { Rhai's scripting engine is very lightweight. It gets its ability from the functions in your program. To call these functions, you need to register them with the scripting engine. ```rust -use rhai::{Dynamic, Engine, RegisterFn}; +use rhai::Engine; +use rhai::RegisterFn; // include the `RegisterFn` trait to use `register_fn` +use rhai::{Dynamic, RegisterDynamicFn}; // include the `RegisterDynamicFn` trait to use `register_dynamic_fn` // Normal function fn add(x: i64, y: i64) -> i64 { @@ -234,7 +236,7 @@ fn decide(yes_no: bool) -> Dynamic { } ``` -# Working with generic functions +# Generic functions Generic functions can be used in Rhai, but you'll need to register separate instances for each concrete type: @@ -258,7 +260,39 @@ fn main() { You can also see in this example how you can register multiple functions (or in this case multiple instances of the same function) to the same name in script. This gives you a way to overload functions and call the correct one, based on the types of the arguments, from your script. -# Override built-in functions +# Fallible functions + +If your function is _fallible_ (i.e. it returns a `Result<_, Error>`), you can register it with `register_result_fn` (using the `RegisterResultFn` trait). + +Your function must return `Result<_, EvalAltResult>`. `EvalAltResult` implements `From<&str>` and `From` etc. and the error text gets converted into `EvalAltResult::ErrorRuntime`. + +```rust +use rhai::{Engine, EvalAltResult, Position}; +use rhai::RegisterResultFn; // include the `RegisterResultFn` trait to use `register_result_fn` + +// Function that may fail +fn safe_divide(x: i64, y: i64) -> Result { + if y == 0 { + // Return an error if y is zero + Err("Division by zero detected!".into()) // short-cut to create EvalAltResult + } else { + Ok(x / y) + } +} + +fn main() { + let mut engine = Engine::new(); + + // Fallible functions that return Result values must use register_result_fn() + engine.register_result_fn("divide", safe_divide); + + if let Err(error) = engine.eval::("divide(40, 0)") { + println!("Error: {:?}", error); // prints ErrorRuntime("Division by zero detected!", (1, 1)") + } +} +``` + +# Overriding built-in functions Any similarly-named function defined in a script overrides any built-in function. diff --git a/src/builtin.rs b/src/builtin.rs index 66789ef3..5a24db0a 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -3,9 +3,15 @@ use crate::any::Any; use crate::engine::{Array, Engine}; -use crate::fn_register::RegisterFn; +use crate::fn_register::{RegisterFn, RegisterResultFn}; +use crate::parser::Position; +use crate::result::EvalAltResult; +use num_traits::{ + CheckedAdd, CheckedDiv, CheckedMul, CheckedNeg, CheckedRem, CheckedShl, CheckedShr, CheckedSub, +}; +use std::convert::TryFrom; use std::fmt::{Debug, Display}; -use std::ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Neg, Range, Rem, Shl, Shr, Sub}; +use std::ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Neg, Range, Rem, Sub}; macro_rules! reg_op { ($self:expr, $x:expr, $op:expr, $( $y:ty ),*) => ( @@ -15,6 +21,22 @@ macro_rules! reg_op { ) } +macro_rules! reg_op_result { + ($self:expr, $x:expr, $op:expr, $( $y:ty ),*) => ( + $( + $self.register_result_fn($x, $op as fn(x: $y, y: $y)->Result<$y,EvalAltResult>); + )* + ) +} + +macro_rules! reg_op_result1 { + ($self:expr, $x:expr, $op:expr, $v:ty, $( $y:ty ),*) => ( + $( + $self.register_result_fn($x, $op as fn(x: $y, y: $v)->Result<$y,EvalAltResult>); + )* + ) +} + macro_rules! reg_un { ($self:expr, $x:expr, $op:expr, $( $y:ty ),*) => ( $( @@ -23,6 +45,13 @@ macro_rules! reg_un { ) } +macro_rules! reg_un_result { + ($self:expr, $x:expr, $op:expr, $( $y:ty ),*) => ( + $( + $self.register_result_fn($x, $op as fn(x: $y)->Result<$y,EvalAltResult>); + )* + ) +} macro_rules! reg_cmp { ($self:expr, $x:expr, $op:expr, $( $y:ty ),*) => ( $( @@ -69,21 +98,96 @@ macro_rules! reg_func3 { impl Engine<'_> { /// Register the core built-in library. pub(crate) fn register_core_lib(&mut self) { - fn add(x: T, y: T) -> ::Output { + fn add(x: T, y: T) -> Result { + x.checked_add(&y).ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Addition overflow: {} + {}", x, y), + Position::none(), + ) + }) + } + fn sub(x: T, y: T) -> Result { + x.checked_sub(&y).ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Subtraction underflow: {} - {}", x, y), + Position::none(), + ) + }) + } + fn mul(x: T, y: T) -> Result { + x.checked_mul(&y).ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Multiplication overflow: {} * {}", x, y), + Position::none(), + ) + }) + } + fn div(x: T, y: T) -> Result + where + T: Display + CheckedDiv + PartialEq + TryFrom, + { + if y == >::try_from(0) + .map_err(|_| ()) + .expect("zero should always succeed") + { + return Err(EvalAltResult::ErrorArithmetic( + format!("Division by zero: {} / {}", x, y), + Position::none(), + )); + } + + x.checked_div(&y).ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Division overflow: {} / {}", x, y), + Position::none(), + ) + }) + } + fn neg(x: T) -> Result { + x.checked_neg().ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Negation overflow: -{}", x), + Position::none(), + ) + }) + } + fn abs>(x: T) -> Result { + if x >= 0.into() { + Ok(x) + } else { + x.checked_neg().ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Negation overflow: -{}", x), + Position::none(), + ) + }) + } + } + fn add_unchecked(x: T, y: T) -> ::Output { x + y } - fn sub(x: T, y: T) -> ::Output { + fn sub_unchecked(x: T, y: T) -> ::Output { x - y } - fn mul(x: T, y: T) -> ::Output { + fn mul_unchecked(x: T, y: T) -> ::Output { x * y } - fn div(x: T, y: T) -> ::Output { + fn div_unchecked(x: T, y: T) -> ::Output { x / y } - fn neg(x: T) -> ::Output { + fn neg_unchecked(x: T) -> ::Output { -x } + fn abs_unchecked>(x: T) -> T + where + ::Output: Into, + { + if x < 0.into() { + (-x).into() + } else { + x + } + } fn lt(x: T, y: T) -> bool { x < y } @@ -120,13 +224,45 @@ impl Engine<'_> { fn binary_xor(x: T, y: T) -> ::Output { x ^ y } - fn left_shift>(x: T, y: T) -> >::Output { - x.shl(y) + fn left_shift(x: T, y: i64) -> Result { + if y < 0 { + return Err(EvalAltResult::ErrorArithmetic( + format!("Left-shift by a negative number: {} << {}", x, y), + Position::none(), + )); + } + + CheckedShl::checked_shl(&x, y as u32).ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Left-shift overflow: {} << {}", x, y), + Position::none(), + ) + }) } - fn right_shift>(x: T, y: T) -> >::Output { - x.shr(y) + fn right_shift(x: T, y: i64) -> Result { + if y < 0 { + return Err(EvalAltResult::ErrorArithmetic( + format!("Right-shift by a negative number: {} >> {}", x, y), + Position::none(), + )); + } + + CheckedShr::checked_shr(&x, y as u32).ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Right-shift overflow: {} % {}", x, y), + Position::none(), + ) + }) } - fn modulo>(x: T, y: T) -> >::Output { + fn modulo(x: T, y: T) -> Result { + x.checked_rem(&y).ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Modulo division overflow: {} % {}", x, y), + Position::none(), + ) + }) + } + fn modulo_unchecked(x: T, y: T) -> ::Output { x % y } fn pow_i64_i64(x: i64, y: i64) -> i64 { @@ -139,10 +275,15 @@ impl Engine<'_> { x.powi(y as i32) } - reg_op!(self, "+", add, i8, u8, i16, u16, i32, i64, u32, u64, f32, f64); - reg_op!(self, "-", sub, i8, u8, i16, u16, i32, i64, u32, u64, f32, f64); - reg_op!(self, "*", mul, i8, u8, i16, u16, i32, i64, u32, u64, f32, f64); - reg_op!(self, "/", div, i8, u8, i16, u16, i32, i64, u32, u64, f32, f64); + reg_op_result!(self, "+", add, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op_result!(self, "-", sub, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op_result!(self, "*", mul, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op_result!(self, "/", div, i8, u8, i16, u16, i32, i64, u32, u64); + + reg_op!(self, "+", add_unchecked, f32, f64); + reg_op!(self, "-", sub_unchecked, f32, f64); + reg_op!(self, "*", mul_unchecked, f32, f64); + reg_op!(self, "/", div_unchecked, f32, f64); reg_cmp!(self, "<", lt, i8, u8, i16, u16, i32, i64, u32, u64, f32, f64, String, char); reg_cmp!(self, "<=", lte, i8, u8, i16, u16, i32, i64, u32, u64, f32, f64, String, char); @@ -162,15 +303,19 @@ impl Engine<'_> { reg_op!(self, "&", binary_and, i8, u8, i16, u16, i32, i64, u32, u64); reg_op!(self, "&", and, bool); reg_op!(self, "^", binary_xor, i8, u8, i16, u16, i32, i64, u32, u64); - reg_op!(self, "<<", left_shift, i8, u8, i16, u16, i32, i64, u32, u64); - reg_op!(self, ">>", right_shift, i8, u8, i16, u16); - reg_op!(self, ">>", right_shift, i32, i64, u32, u64); - reg_op!(self, "%", modulo, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op_result1!(self, "<<", left_shift, i64, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op_result1!(self, ">>", right_shift, i64, i8, u8, i16, u16); + reg_op_result1!(self, ">>", right_shift, i64, i32, i64, u32, u64); + reg_op_result!(self, "%", modulo, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op!(self, "%", modulo_unchecked, f32, f64); self.register_fn("~", pow_i64_i64); self.register_fn("~", pow_f64_f64); self.register_fn("~", pow_f64_i64); - reg_un!(self, "-", neg, i8, i16, i32, i64, f32, f64); + reg_un_result!(self, "-", neg, i8, i16, i32, i64); + reg_un!(self, "-", neg_unchecked, f32, f64); + reg_un_result!(self, "abs", abs, i8, i16, i32, i64); + reg_un!(self, "abs", abs_unchecked, f32, f64); reg_un!(self, "!", not, bool); self.register_fn("+", |x: String, y: String| x + &y); // String + String diff --git a/src/error.rs b/src/error.rs index 41a793c3..b631c4cf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -143,6 +143,9 @@ impl fmt::Display for ParseError { if !self.1.is_eof() { write!(f, " ({})", self.1) + } else if !self.1.is_none() { + // Do not write any position if None + Ok(()) } else { write!(f, " at the end of the script but there is no more input") } diff --git a/src/fn_register.rs b/src/fn_register.rs index 535ffc83..a213474a 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -58,6 +58,32 @@ pub trait RegisterDynamicFn { fn register_dynamic_fn(&mut self, name: &str, f: FN); } +/// A trait to register fallible custom functions returning Result<_, EvalAltResult> with the `Engine`. +/// +/// # Example +/// +/// ```rust +/// use rhai::{Engine, RegisterFn}; +/// +/// // Normal function +/// fn add(x: i64, y: i64) -> i64 { +/// x + y +/// } +/// +/// let mut engine = Engine::new(); +/// +/// // You must use the trait rhai::RegisterFn to get this method. +/// engine.register_fn("add", add); +/// +/// if let Ok(result) = engine.eval::("add(40, 2)") { +/// println!("Answer: {}", result); // prints 42 +/// } +/// ``` +pub trait RegisterResultFn { + /// Register a custom function with the `Engine`. + fn register_result_fn(&mut self, name: &str, f: FN); +} + pub struct Ref(A); pub struct Mut(A); @@ -91,7 +117,7 @@ macro_rules! def_register { let mut drain = args.drain(..); $( // Downcast every element, return in case of a type mismatch - let $par = (drain.next().unwrap().downcast_mut() as Option<&mut $par>).unwrap(); + let $par = drain.next().unwrap().downcast_mut::<$par>().unwrap(); )* // Call the user-supplied function using ($clone) to @@ -123,7 +149,7 @@ macro_rules! def_register { let mut drain = args.drain(..); $( // Downcast every element, return in case of a type mismatch - let $par = (drain.next().unwrap().downcast_mut() as Option<&mut $par>).unwrap(); + let $par = drain.next().unwrap().downcast_mut::<$par>().unwrap(); )* // Call the user-supplied function using ($clone) to @@ -135,6 +161,44 @@ macro_rules! def_register { } } + impl< + $($par: Any + Clone,)* + FN: Fn($($param),*) -> Result + 'static, + RET: Any + > RegisterResultFn for Engine<'_> + { + fn register_result_fn(&mut self, name: &str, f: FN) { + let fn_name = name.to_string(); + + let fun = move |mut args: FnCallArgs, pos: Position| { + // Check for length at the beginning to avoid per-element bound checks. + const NUM_ARGS: usize = count_args!($($par)*); + + if args.len() != NUM_ARGS { + Err(EvalAltResult::ErrorFunctionArgsMismatch(fn_name.clone(), NUM_ARGS, args.len(), pos)) + } else { + #[allow(unused_variables, unused_mut)] + let mut drain = args.drain(..); + $( + // Downcast every element, return in case of a type mismatch + let $par = drain.next().unwrap().downcast_mut::<$par>().unwrap(); + )* + + // Call the user-supplied function using ($clone) to + // potentially clone the value, otherwise pass the reference. + match f($(($clone)($par)),*) { + Ok(r) => Ok(Box::new(r) as Dynamic), + Err(mut err) => { + err.set_position(pos); + Err(err) + } + } + } + }; + self.register_fn_raw(name, Some(vec![$(TypeId::of::<$par>()),*]), Box::new(fun)); + } + } + //def_register!(imp_pop $($par => $mark => $param),*); }; ($p0:ident $(, $p:ident)*) => { diff --git a/src/lib.rs b/src/lib.rs index 5c947e95..541963c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,7 +69,7 @@ pub use any::{Any, AnyExt, Dynamic, Variant}; pub use call::FuncArgs; pub use engine::{Array, Engine}; pub use error::{ParseError, ParseErrorType}; -pub use fn_register::{RegisterDynamicFn, RegisterFn}; +pub use fn_register::{RegisterDynamicFn, RegisterFn, RegisterResultFn}; pub use parser::{Position, AST}; pub use result::EvalAltResult; pub use scope::Scope; diff --git a/src/parser.rs b/src/parser.rs index ad714187..13afb96b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,7 +2,7 @@ use crate::any::Dynamic; use crate::error::{LexError, ParseError, ParseErrorType}; -use std::{borrow::Cow, char, fmt, iter::Peekable, str::Chars}; +use std::{borrow::Cow, char, fmt, iter::Peekable, str::Chars, usize}; type LERR = LexError; type PERR = ParseErrorType; @@ -17,25 +17,33 @@ pub struct Position { impl Position { /// Create a new `Position`. pub fn new(line: usize, position: usize) -> Self { + if line == 0 || (line == usize::MAX && position == usize::MAX) { + panic!("invalid position: ({}, {})", line, position); + } + Self { line, pos: position, } } - /// Get the line number (1-based), or `None` if EOF. + /// Get the line number (1-based), or `None` if no position or EOF. pub fn line(&self) -> Option { - match self.line { - 0 => None, - x => Some(x), + if self.is_none() || self.is_eof() { + None + } else { + Some(self.line) } } /// Get the character position (1-based), or `None` if at beginning of a line. pub fn position(&self) -> Option { - match self.pos { - 0 => None, - x => Some(x), + if self.is_none() || self.is_eof() { + None + } else if self.pos == 0 { + None + } else { + Some(self.pos) } } @@ -61,14 +69,27 @@ impl Position { self.pos = 0; } + /// Create a `Position` representing no position. + pub(crate) fn none() -> Self { + Self { line: 0, pos: 0 } + } + /// Create a `Position` at EOF. pub(crate) fn eof() -> Self { - Self { line: 0, pos: 0 } + Self { + line: usize::MAX, + pos: usize::MAX, + } + } + + /// Is there no `Position`? + pub fn is_none(&self) -> bool { + self.line == 0 && self.pos == 0 } /// Is the `Position` at EOF? pub fn is_eof(&self) -> bool { - self.line == 0 + self.line == usize::MAX && self.pos == usize::MAX } } @@ -82,6 +103,8 @@ impl fmt::Display for Position { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.is_eof() { write!(f, "EOF") + } else if self.is_none() { + write!(f, "none") } else { write!(f, "line {}, position {}", self.line, self.pos) } diff --git a/src/result.rs b/src/result.rs index 26272030..95b5ee12 100644 --- a/src/result.rs +++ b/src/result.rs @@ -118,9 +118,10 @@ impl fmt::Display for EvalAltResult { Self::ErrorMismatchOutputType(s, pos) => write!(f, "{}: {} ({})", desc, s, pos), Self::ErrorDotExpr(s, pos) if !s.is_empty() => 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::ErrorArithmetic(s, pos) => write!(f, "{} ({})", s, pos), + Self::ErrorRuntime(s, pos) => { + write!(f, "{} ({})", if s.is_empty() { desc } else { s }, pos) + } Self::LoopBreak => write!(f, "{}", desc), Self::Return(_, pos) => write!(f, "{} ({})", desc, pos), Self::ErrorReadingScriptFile(filename, err) => { @@ -171,3 +172,37 @@ impl From for EvalAltResult { Self::ErrorParsing(err) } } + +impl EvalAltResult { + pub(crate) fn set_position(&mut self, new_position: Position) { + match self { + EvalAltResult::ErrorReadingScriptFile(_, _) + | EvalAltResult::LoopBreak + | EvalAltResult::ErrorParsing(_) => (), + + EvalAltResult::ErrorFunctionNotFound(_, ref mut pos) + | EvalAltResult::ErrorFunctionArgsMismatch(_, _, _, ref mut pos) + | EvalAltResult::ErrorBooleanArgMismatch(_, ref mut pos) + | EvalAltResult::ErrorCharMismatch(ref mut pos) + | EvalAltResult::ErrorArrayBounds(_, _, ref mut pos) + | EvalAltResult::ErrorStringBounds(_, _, ref mut pos) + | EvalAltResult::ErrorIndexingType(_, ref mut pos) + | EvalAltResult::ErrorIndexExpr(ref mut pos) + | EvalAltResult::ErrorIfGuard(ref mut pos) + | EvalAltResult::ErrorFor(ref mut pos) + | EvalAltResult::ErrorVariableNotFound(_, ref mut pos) + | EvalAltResult::ErrorAssignmentToUnknownLHS(ref mut pos) + | EvalAltResult::ErrorMismatchOutputType(_, ref mut pos) + | EvalAltResult::ErrorDotExpr(_, ref mut pos) + | EvalAltResult::ErrorArithmetic(_, ref mut pos) + | EvalAltResult::ErrorRuntime(_, ref mut pos) + | EvalAltResult::Return(_, ref mut pos) => *pos = new_position, + } + } +} + +impl> From for EvalAltResult { + fn from(err: T) -> Self { + Self::ErrorRuntime(err.as_ref().to_string(), Position::none()) + } +} diff --git a/tests/math.rs b/tests/math.rs new file mode 100644 index 00000000..bc79cbb0 --- /dev/null +++ b/tests/math.rs @@ -0,0 +1,40 @@ +use rhai::{Engine, EvalAltResult}; + +#[test] +fn test_math() -> Result<(), EvalAltResult> { + let mut engine = Engine::new(); + + assert_eq!(engine.eval::("1 + 2")?, 3); + assert_eq!(engine.eval::("1 - 2")?, -1); + assert_eq!(engine.eval::("2 * 3")?, 6); + assert_eq!(engine.eval::("1 / 2")?, 0); + assert_eq!(engine.eval::("3 % 2")?, 1); + assert_eq!( + engine.eval::("(-9223372036854775807).abs()")?, + 9223372036854775807 + ); + + // Overflow/underflow/division-by-zero errors + match engine.eval::("9223372036854775807 + 1") { + Err(EvalAltResult::ErrorArithmetic(_, _)) => (), + r => panic!("should return overflow error: {:?}", r), + } + match engine.eval::("(-9223372036854775807) - 2") { + Err(EvalAltResult::ErrorArithmetic(_, _)) => (), + r => panic!("should return underflow error: {:?}", r), + } + match engine.eval::("9223372036854775807 * 9223372036854775807") { + Err(EvalAltResult::ErrorArithmetic(_, _)) => (), + r => panic!("should return overflow error: {:?}", r), + } + match engine.eval::("9223372036854775807 / 0") { + Err(EvalAltResult::ErrorArithmetic(_, _)) => (), + r => panic!("should return division by zero error: {:?}", r), + } + match engine.eval::("9223372036854775807 % 0") { + Err(EvalAltResult::ErrorArithmetic(_, _)) => (), + r => panic!("should return division by zero error: {:?}", r), + } + + Ok(()) +} From c5b40783ef80b81d2436475dba1f7a18c4927bca Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 8 Mar 2020 23:14:18 +0800 Subject: [PATCH 14/27] Add unchecked feature to remove arithmetic operations checking. --- Cargo.toml | 3 +- README.md | 6 ++- src/builtin.rs | 127 ++++++++++++++++++++++++++++++++++++------------- tests/math.rs | 41 ++++++++-------- 4 files changed, 124 insertions(+), 53 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e39a0115..fe543553 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,5 @@ num-traits = "*" [features] debug_msgs = [] -no_stdlib = [] \ No newline at end of file +no_stdlib = [] +unchecked = [] diff --git a/README.md b/README.md index 9eaacd38..ebfc0909 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Rhai's current feature set: * Low compile-time overhead (~0.6 sec debug/~3 sec release for script runner app) * Easy-to-use language similar to JS+Rust * Support for overloaded functions -* No additional dependencies +* Very few additional dependencies (right now only `num-traits` to do checked arithmetic operations) **Note:** Currently, the version is 0.10.2, so the language and API's may change before they stabilize. @@ -43,6 +43,10 @@ Print debug messages to stdout (using `println!`) related to function registrati Exclude the standard library of utility functions in the build, and only include the minimum necessary functionalities. +### `unchecked` + +Exclude arithmetic checking in the standard library. Beware that a bad script may panic the entire system! + ## Related Other cool projects to check out: diff --git a/src/builtin.rs b/src/builtin.rs index 5a24db0a..e7cfb45b 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -3,15 +3,23 @@ use crate::any::Any; use crate::engine::{Array, Engine}; -use crate::fn_register::{RegisterFn, RegisterResultFn}; -use crate::parser::Position; -use crate::result::EvalAltResult; +use crate::fn_register::RegisterFn; +use std::fmt::{Debug, Display}; +use std::ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Neg, Range, Rem, Sub}; + +#[cfg(feature = "unchecked")] +use std::ops::{Shl, Shr}; + +#[cfg(not(feature = "unchecked"))] +use crate::{parser::Position, result::EvalAltResult, RegisterResultFn}; + +#[cfg(not(feature = "unchecked"))] +use std::convert::TryFrom; + +#[cfg(not(feature = "unchecked"))] use num_traits::{ CheckedAdd, CheckedDiv, CheckedMul, CheckedNeg, CheckedRem, CheckedShl, CheckedShr, CheckedSub, }; -use std::convert::TryFrom; -use std::fmt::{Debug, Display}; -use std::ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Neg, Range, Rem, Sub}; macro_rules! reg_op { ($self:expr, $x:expr, $op:expr, $( $y:ty ),*) => ( @@ -21,6 +29,7 @@ macro_rules! reg_op { ) } +#[cfg(not(feature = "unchecked"))] macro_rules! reg_op_result { ($self:expr, $x:expr, $op:expr, $( $y:ty ),*) => ( $( @@ -29,6 +38,7 @@ macro_rules! reg_op_result { ) } +#[cfg(not(feature = "unchecked"))] macro_rules! reg_op_result1 { ($self:expr, $x:expr, $op:expr, $v:ty, $( $y:ty ),*) => ( $( @@ -45,6 +55,7 @@ macro_rules! reg_un { ) } +#[cfg(not(feature = "unchecked"))] macro_rules! reg_un_result { ($self:expr, $x:expr, $op:expr, $( $y:ty ),*) => ( $( @@ -98,6 +109,7 @@ macro_rules! reg_func3 { impl Engine<'_> { /// Register the core built-in library. pub(crate) fn register_core_lib(&mut self) { + #[cfg(not(feature = "unchecked"))] fn add(x: T, y: T) -> Result { x.checked_add(&y).ok_or_else(|| { EvalAltResult::ErrorArithmetic( @@ -106,6 +118,7 @@ impl Engine<'_> { ) }) } + #[cfg(not(feature = "unchecked"))] fn sub(x: T, y: T) -> Result { x.checked_sub(&y).ok_or_else(|| { EvalAltResult::ErrorArithmetic( @@ -114,6 +127,7 @@ impl Engine<'_> { ) }) } + #[cfg(not(feature = "unchecked"))] fn mul(x: T, y: T) -> Result { x.checked_mul(&y).ok_or_else(|| { EvalAltResult::ErrorArithmetic( @@ -122,6 +136,7 @@ impl Engine<'_> { ) }) } + #[cfg(not(feature = "unchecked"))] fn div(x: T, y: T) -> Result where T: Display + CheckedDiv + PartialEq + TryFrom, @@ -143,6 +158,7 @@ impl Engine<'_> { ) }) } + #[cfg(not(feature = "unchecked"))] fn neg(x: T) -> Result { x.checked_neg().ok_or_else(|| { EvalAltResult::ErrorArithmetic( @@ -151,6 +167,7 @@ impl Engine<'_> { ) }) } + #[cfg(not(feature = "unchecked"))] fn abs>(x: T) -> Result { if x >= 0.into() { Ok(x) @@ -163,22 +180,22 @@ impl Engine<'_> { }) } } - fn add_unchecked(x: T, y: T) -> ::Output { + fn add_u(x: T, y: T) -> ::Output { x + y } - fn sub_unchecked(x: T, y: T) -> ::Output { + fn sub_u(x: T, y: T) -> ::Output { x - y } - fn mul_unchecked(x: T, y: T) -> ::Output { + fn mul_u(x: T, y: T) -> ::Output { x * y } - fn div_unchecked(x: T, y: T) -> ::Output { + fn div_u(x: T, y: T) -> ::Output { x / y } - fn neg_unchecked(x: T) -> ::Output { + fn neg_u(x: T) -> ::Output { -x } - fn abs_unchecked>(x: T) -> T + fn abs_u>(x: T) -> T where ::Output: Into, { @@ -224,7 +241,8 @@ impl Engine<'_> { fn binary_xor(x: T, y: T) -> ::Output { x ^ y } - fn left_shift(x: T, y: i64) -> Result { + #[cfg(not(feature = "unchecked"))] + fn shl(x: T, y: i64) -> Result { if y < 0 { return Err(EvalAltResult::ErrorArithmetic( format!("Left-shift by a negative number: {} << {}", x, y), @@ -239,7 +257,8 @@ impl Engine<'_> { ) }) } - fn right_shift(x: T, y: i64) -> Result { + #[cfg(not(feature = "unchecked"))] + fn shr(x: T, y: i64) -> Result { if y < 0 { return Err(EvalAltResult::ErrorArithmetic( format!("Right-shift by a negative number: {} >> {}", x, y), @@ -254,6 +273,15 @@ impl Engine<'_> { ) }) } + #[cfg(feature = "unchecked")] + fn shl_u>(x: T, y: T) -> >::Output { + x.shl(y) + } + #[cfg(feature = "unchecked")] + fn shr_u>(x: T, y: T) -> >::Output { + x.shr(y) + } + #[cfg(not(feature = "unchecked"))] fn modulo(x: T, y: T) -> Result { x.checked_rem(&y).ok_or_else(|| { EvalAltResult::ErrorArithmetic( @@ -262,7 +290,7 @@ impl Engine<'_> { ) }) } - fn modulo_unchecked(x: T, y: T) -> ::Output { + fn modulo_u(x: T, y: T) -> ::Output { x % y } fn pow_i64_i64(x: i64, y: i64) -> i64 { @@ -275,15 +303,26 @@ impl Engine<'_> { x.powi(y as i32) } - reg_op_result!(self, "+", add, i8, u8, i16, u16, i32, i64, u32, u64); - reg_op_result!(self, "-", sub, i8, u8, i16, u16, i32, i64, u32, u64); - reg_op_result!(self, "*", mul, i8, u8, i16, u16, i32, i64, u32, u64); - reg_op_result!(self, "/", div, i8, u8, i16, u16, i32, i64, u32, u64); + #[cfg(not(feature = "unchecked"))] + { + reg_op_result!(self, "+", add, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op_result!(self, "-", sub, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op_result!(self, "*", mul, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op_result!(self, "/", div, i8, u8, i16, u16, i32, i64, u32, u64); + } - reg_op!(self, "+", add_unchecked, f32, f64); - reg_op!(self, "-", sub_unchecked, f32, f64); - reg_op!(self, "*", mul_unchecked, f32, f64); - reg_op!(self, "/", div_unchecked, f32, f64); + #[cfg(feature = "unchecked")] + { + reg_op!(self, "+", add_u, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op!(self, "-", sub_u, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op!(self, "*", mul_u, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op!(self, "/", div_u, i8, u8, i16, u16, i32, i64, u32, u64); + } + + reg_op!(self, "+", add_u, f32, f64); + reg_op!(self, "-", sub_u, f32, f64); + reg_op!(self, "*", mul_u, f32, f64); + reg_op!(self, "/", div_u, f32, f64); reg_cmp!(self, "<", lt, i8, u8, i16, u16, i32, i64, u32, u64, f32, f64, String, char); reg_cmp!(self, "<=", lte, i8, u8, i16, u16, i32, i64, u32, u64, f32, f64, String, char); @@ -303,19 +342,43 @@ impl Engine<'_> { reg_op!(self, "&", binary_and, i8, u8, i16, u16, i32, i64, u32, u64); reg_op!(self, "&", and, bool); reg_op!(self, "^", binary_xor, i8, u8, i16, u16, i32, i64, u32, u64); - reg_op_result1!(self, "<<", left_shift, i64, i8, u8, i16, u16, i32, i64, u32, u64); - reg_op_result1!(self, ">>", right_shift, i64, i8, u8, i16, u16); - reg_op_result1!(self, ">>", right_shift, i64, i32, i64, u32, u64); - reg_op_result!(self, "%", modulo, i8, u8, i16, u16, i32, i64, u32, u64); - reg_op!(self, "%", modulo_unchecked, f32, f64); + + #[cfg(not(feature = "unchecked"))] + { + reg_op_result1!(self, "<<", shl, i64, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op_result1!(self, ">>", shr, i64, i8, u8, i16, u16); + reg_op_result1!(self, ">>", shr, i64, i32, i64, u32, u64); + reg_op_result!(self, "%", modulo, i8, u8, i16, u16, i32, i64, u32, u64); + } + + #[cfg(feature = "unchecked")] + { + reg_op!(self, "<<", shl_u, i64, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op!(self, ">>", shr_u, i64, i8, u8, i16, u16); + reg_op!(self, ">>", shr_u, i64, i32, i64, u32, u64); + reg_op!(self, "%", modulo_u, i8, u8, i16, u16, i32, i64, u32, u64); + } + + reg_op!(self, "%", modulo_u, f32, f64); + self.register_fn("~", pow_i64_i64); self.register_fn("~", pow_f64_f64); self.register_fn("~", pow_f64_i64); - reg_un_result!(self, "-", neg, i8, i16, i32, i64); - reg_un!(self, "-", neg_unchecked, f32, f64); - reg_un_result!(self, "abs", abs, i8, i16, i32, i64); - reg_un!(self, "abs", abs_unchecked, f32, f64); + #[cfg(not(feature = "unchecked"))] + { + reg_un_result!(self, "-", neg, i8, i16, i32, i64); + reg_un_result!(self, "abs", abs, i8, i16, i32, i64); + } + + #[cfg(feature = "unchecked")] + { + reg_un!(self, "-", neg_u, i8, i16, i32, i64); + reg_un!(self, "abs", abs_u, i8, i16, i32, i64); + } + + reg_un!(self, "-", neg_u, f32, f64); + reg_un!(self, "abs", abs_u, f32, f64); reg_un!(self, "!", not, bool); self.register_fn("+", |x: String, y: String| x + &y); // String + String diff --git a/tests/math.rs b/tests/math.rs index bc79cbb0..98836037 100644 --- a/tests/math.rs +++ b/tests/math.rs @@ -15,25 +15,28 @@ fn test_math() -> Result<(), EvalAltResult> { ); // Overflow/underflow/division-by-zero errors - match engine.eval::("9223372036854775807 + 1") { - Err(EvalAltResult::ErrorArithmetic(_, _)) => (), - r => panic!("should return overflow error: {:?}", r), - } - match engine.eval::("(-9223372036854775807) - 2") { - Err(EvalAltResult::ErrorArithmetic(_, _)) => (), - r => panic!("should return underflow error: {:?}", r), - } - match engine.eval::("9223372036854775807 * 9223372036854775807") { - Err(EvalAltResult::ErrorArithmetic(_, _)) => (), - r => panic!("should return overflow error: {:?}", r), - } - match engine.eval::("9223372036854775807 / 0") { - Err(EvalAltResult::ErrorArithmetic(_, _)) => (), - r => panic!("should return division by zero error: {:?}", r), - } - match engine.eval::("9223372036854775807 % 0") { - Err(EvalAltResult::ErrorArithmetic(_, _)) => (), - r => panic!("should return division by zero error: {:?}", r), + #[cfg(not(feature = "unchecked"))] + { + match engine.eval::("9223372036854775807 + 1") { + Err(EvalAltResult::ErrorArithmetic(_, _)) => (), + r => panic!("should return overflow error: {:?}", r), + } + match engine.eval::("(-9223372036854775807) - 2") { + Err(EvalAltResult::ErrorArithmetic(_, _)) => (), + r => panic!("should return underflow error: {:?}", r), + } + match engine.eval::("9223372036854775807 * 9223372036854775807") { + Err(EvalAltResult::ErrorArithmetic(_, _)) => (), + r => panic!("should return overflow error: {:?}", r), + } + match engine.eval::("9223372036854775807 / 0") { + Err(EvalAltResult::ErrorArithmetic(_, _)) => (), + r => panic!("should return division by zero error: {:?}", r), + } + match engine.eval::("9223372036854775807 % 0") { + Err(EvalAltResult::ErrorArithmetic(_, _)) => (), + r => panic!("should return division by zero error: {:?}", r), + } } Ok(()) From 01d04f717be237a037e699cfd668efe8aa20af06 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 9 Mar 2020 10:10:19 +0800 Subject: [PATCH 15/27] Natively handle negative numbers in tokenizer instead of the neg() function. --- src/parser.rs | 64 +++++++++++++++++++++++++++++--------------- tests/math.rs | 2 +- tests/unary_minus.rs | 4 +-- 3 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 13afb96b..5fe02586 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -338,6 +338,7 @@ impl Token { use self::Token::*; match *self { + LexError(_) | LeftBrace | // (+expr) - is unary // RightBrace | {expr} - expr not unary & is closing LeftParen | // {-expr} - is unary @@ -386,6 +387,7 @@ impl Token { PowerOf | In | PowerOfAssign => true, + _ => false, } } @@ -398,6 +400,7 @@ impl Token { RightBrace | RightParen | RightBracket | Plus | Minus | Multiply | Divide | Comma | Equals | LessThan | GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo | NotEqualsTo | Pipe | Or | Ampersand | And | PowerOf => true, + _ => false, } } @@ -567,6 +570,8 @@ impl<'a> TokenIterator<'a> { } fn inner_next(&mut self) -> Option<(Token, Position)> { + let mut negated = false; + while let Some(c) = self.char_stream.next() { self.advance(); @@ -653,6 +658,10 @@ impl<'a> TokenIterator<'a> { } } + if negated { + result.insert(0, '-'); + } + if let Some(radix) = radix_base { let out: String = result.iter().skip(2).filter(|&&c| c != '_').collect(); @@ -761,20 +770,17 @@ impl<'a> TokenIterator<'a> { pos, )) } - '-' => { - return Some(( - match self.char_stream.peek() { - Some(&'=') => { - self.char_stream.next(); - self.advance(); - Token::MinusAssign - } - _ if self.last.is_next_unary() => Token::UnaryMinus, - _ => Token::Minus, - }, - pos, - )) - } + '-' => match self.char_stream.peek() { + // Negative number? + Some('0'..='9') => negated = true, + Some('=') => { + self.char_stream.next(); + self.advance(); + return Some((Token::MinusAssign, pos)); + } + _ if self.last.is_next_unary() => return Some((Token::UnaryMinus, pos)), + _ => return Some((Token::Minus, pos)), + }, '*' => { return Some(( match self.char_stream.peek() { @@ -1034,19 +1040,28 @@ fn get_precedence(token: &Token) -> i8 { | Token::XOrAssign | Token::ModuloAssign | Token::PowerOfAssign => 10, + Token::Or | Token::XOr | Token::Pipe => 11, + Token::And | Token::Ampersand => 12, + Token::LessThan | Token::LessThanEqualsTo | Token::GreaterThan | Token::GreaterThanEqualsTo | Token::EqualsTo | Token::NotEqualsTo => 15, + Token::Plus | Token::Minus => 20, + Token::Divide | Token::Multiply | Token::PowerOf => 40, + Token::LeftShift | Token::RightShift => 50, + Token::Modulo => 60, + Token::Period => 100, + _ => -1, } } @@ -1056,6 +1071,7 @@ fn parse_paren_expr<'a>( begin: Position, ) -> Result { match input.peek() { + // () Some((Token::RightParen, _)) => { input.next(); return Ok(Expr::Unit(begin)); @@ -1272,16 +1288,22 @@ fn parse_unary<'a>(input: &mut Peekable>) -> Result { input.next(); - Ok(Expr::FunctionCall( - "-".into(), - vec![parse_primary(input)?], - None, - pos, - )) + match parse_unary(input) { + // Negative integer + Ok(Expr::IntegerConstant(i, pos)) => Ok(i + .checked_neg() + .map(|x| Expr::IntegerConstant(x, pos)) + .unwrap_or_else(|| Expr::FloatConstant(-(i as f64), pos))), + // Negative float + Ok(Expr::FloatConstant(f, pos)) => Ok(Expr::FloatConstant(-f, pos)), + // Call negative function + Ok(expr) => Ok(Expr::FunctionCall("-".into(), vec![expr], None, pos)), + err @ Err(_) => err, + } } Some(&(Token::UnaryPlus, _)) => { input.next(); - parse_primary(input) + parse_unary(input) } Some(&(Token::Bang, pos)) => { input.next(); diff --git a/tests/math.rs b/tests/math.rs index 98836037..9b92f1fb 100644 --- a/tests/math.rs +++ b/tests/math.rs @@ -21,7 +21,7 @@ fn test_math() -> Result<(), EvalAltResult> { Err(EvalAltResult::ErrorArithmetic(_, _)) => (), r => panic!("should return overflow error: {:?}", r), } - match engine.eval::("(-9223372036854775807) - 2") { + match engine.eval::("-9223372036854775808 - 1") { Err(EvalAltResult::ErrorArithmetic(_, _)) => (), r => panic!("should return underflow error: {:?}", r), } diff --git a/tests/unary_minus.rs b/tests/unary_minus.rs index b32e6860..37718326 100644 --- a/tests/unary_minus.rs +++ b/tests/unary_minus.rs @@ -5,8 +5,8 @@ fn test_unary_minus() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); assert_eq!(engine.eval::("let x = -5; x")?, -5); - assert_eq!(engine.eval::("fn n(x) { -x } n(5)")?, -5); - assert_eq!(engine.eval::("5 - -(-5)")?, 0); + assert_eq!(engine.eval::("fn neg(x) { -x } neg(5)")?, -5); + assert_eq!(engine.eval::("5 - -+++--+-5")?, 0); Ok(()) } From e54fb54da20863506db4773859263bed85c0bbdd Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 9 Mar 2020 10:41:17 +0800 Subject: [PATCH 16/27] Catch indexing errors at compile time. --- src/error.rs | 14 +++++---- src/parser.rs | 81 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/src/error.rs b/src/error.rs index b631c4cf..5c4419d4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -64,9 +64,9 @@ pub enum ParseErrorType { /// An open `[` is missing the corresponding closing `]`. MissingRightBracket(String), /// An expression in function call arguments `()` has syntax error. - MalformedCallExpr, + MalformedCallExpr(String), /// An expression in indexing brackets `[]` has syntax error. - MalformedIndexExpr, + MalformedIndexExpr(String), /// Missing a variable name after the `let` keyword. VarExpectsIdentifier, /// Defining a function `fn` in an appropriate place (e.g. inside another function). @@ -110,8 +110,8 @@ impl Error for ParseError { ParseErrorType::MissingLeftBrace => "Expecting '{'", ParseErrorType::MissingRightBrace(_) => "Expecting '}'", ParseErrorType::MissingRightBracket(_) => "Expecting ']'", - ParseErrorType::MalformedCallExpr => "Invalid expression in function call arguments", - ParseErrorType::MalformedIndexExpr => "Invalid index in indexing expression", + ParseErrorType::MalformedCallExpr(_) => "Invalid expression in function call arguments", + ParseErrorType::MalformedIndexExpr(_) => "Invalid index in indexing expression", ParseErrorType::VarExpectsIdentifier => "Expecting name of a variable", ParseErrorType::FnMissingName => "Expecting name in function declaration", ParseErrorType::FnMissingParams(_) => "Expecting parameters in function declaration", @@ -128,7 +128,11 @@ impl Error for ParseError { impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { - ParseErrorType::BadInput(ref s) => write!(f, "{}", s)?, + ParseErrorType::BadInput(ref s) + | ParseErrorType::MalformedIndexExpr(ref s) + | ParseErrorType::MalformedCallExpr(ref s) => { + write!(f, "{}", if s.is_empty() { self.description() } else { s })? + } ParseErrorType::UnknownOperator(ref s) => write!(f, "{}: '{}'", self.description(), s)?, ParseErrorType::FnMissingParams(ref s) => { write!(f, "Expecting parameters for function '{}'", s)? diff --git a/src/parser.rs b/src/parser.rs index 5fe02586..84a67007 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1146,7 +1146,66 @@ fn parse_index_expr<'a>( input: &mut Peekable>, pos: Position, ) -> Result { - parse_expr(input).and_then(|idx_expr| match input.peek() { + let idx_expr = parse_expr(input)?; + + // Check type of indexing - must be integer + match &idx_expr { + Expr::IntegerConstant(i, pos) if *i < 0 => { + return Err(ParseError::new( + PERR::MalformedIndexExpr(format!( + "Array access expects non-negative index: {} < 0", + i + )), + *pos, + )) + } + Expr::FloatConstant(_, pos) => { + return Err(ParseError::new( + PERR::MalformedIndexExpr("Array access expects integer index, not a float".into()), + *pos, + )) + } + Expr::CharConstant(_, pos) => { + return Err(ParseError::new( + PERR::MalformedIndexExpr( + "Array access expects integer index, not a character".into(), + ), + *pos, + )) + } + Expr::StringConstant(_, pos) => { + return Err(ParseError::new( + PERR::MalformedIndexExpr("Array access expects integer index, not a string".into()), + *pos, + )) + } + Expr::Assignment(_, _, pos) | Expr::Unit(pos) => { + return Err(ParseError::new( + PERR::MalformedIndexExpr("Array access expects integer index, not ()".into()), + *pos, + )) + } + Expr::And(lhs, _) | Expr::Or(lhs, _) => { + return Err(ParseError::new( + PERR::MalformedIndexExpr( + "Array access expects integer index, not a boolean".into(), + ), + lhs.position(), + )) + } + Expr::True(pos) | Expr::False(pos) => { + return Err(ParseError::new( + PERR::MalformedIndexExpr( + "Array access expects integer index, not a boolean".into(), + ), + *pos, + )) + } + _ => (), + } + + // Check if there is a closing bracket + match input.peek() { Some(&(Token::RightBracket, _)) => { input.next(); return Ok(Expr::Index(lhs, Box::new(idx_expr), pos)); @@ -1163,7 +1222,7 @@ fn parse_index_expr<'a>( Position::eof(), )) } - }) + } } fn parse_ident_expr<'a>( @@ -1728,8 +1787,22 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result { params.push(s.into()); } - Some((_, pos)) => return Err(ParseError::new(PERR::MalformedCallExpr, pos)), - None => return Err(ParseError::new(PERR::MalformedCallExpr, Position::eof())), + Some((_, pos)) => { + return Err(ParseError::new( + PERR::MalformedCallExpr( + "Function call arguments missing either a ',' or a ')'".into(), + ), + pos, + )) + } + None => { + return Err(ParseError::new( + PERR::MalformedCallExpr( + "Function call arguments missing a closing ')'".into(), + ), + Position::eof(), + )) + } } }, } From b9d562eba4c3b8c9391c6b233dde9521190075ec Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 9 Mar 2020 11:42:10 +0800 Subject: [PATCH 17/27] Add standard math functions and make power functions checked. --- README.md | 98 +++++++++++++++++++++++++++++++++++---------- src/builtin.rs | 106 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 176 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index ebfc0909..45371f98 100644 --- a/README.md +++ b/README.md @@ -157,12 +157,14 @@ let result: i64 = engine.call_fn("hello", &ast, (&mut String::from("abc"), &mut The following primitive types are supported natively: -* Integer: `i32`, `u32`, `i64` (default), `u64` -* Floating-point: `f32`, `f64` (default) -* Character: `char` -* Boolean: `bool` -* Array: `rhai::Array` -* Dynamic (i.e. can be anything): `rhai::Dynamic` +| Category | Types | +| ------------------------------ | -------------------------------------- | +| Integer | `i32`, `u32`, `i64` _(default)_, `u64` | +| Floating-point | `f32`, `f64` _(default)_ | +| Character | `char` | +| Boolean | `bool` | +| Array | `rhai::Array` | +| Dynamic (i.e. can be anything) | `rhai::Dynamic` | # Value conversions @@ -508,16 +510,60 @@ fn main() { ## Variables +Variables in `Rhai` follow normal naming rules: + +* Must start with an ASCII letter +* Must contain only ASCII letters, digits and `_` underscores + +Example: + ```rust let x = 3; ``` +## Numbers + +| Format | Type | +| ---------------- | ------------------------------------------------------ | +| `123_345`, `-42` | `i64` in decimal, '`_`' separator can be used anywhere | +| `0o07_76` | `i64` in octal, '`_`' separator can be used anywhere | +| `0xabcd_ef` | `i64` in hex, '`_`' separator can be used anywhere | +| `0b0101_1001` | `i64` in binary, '`_`' separator can be used anywhere | +| `123_456.789` | `f64`, '`_`' separator can be used anywhere | + ## Numeric operators ```rust -let x = (1 + 2) * (6 - 4) / 2; +let x = (1 + 2) * (6 - 4) / 2; // arithmetic +let reminder = 42 % 10; // modulo +let power = 42 ~ 2; // power (i64 and f64 only) +let left_shifted = 42 << 3; // left shift +let right_shifted = 42 >> 3; // right shift +let bit_op = 42 | 99; // bit masking ``` +## Numeric functions + +The following standard functions (defined in the standard library but excluded if you use the `no_stdlib` feature) operate on `i8`, `i16`, `i32`, `i64`, `f32` and `f64` only: + +| Category | Functions | +| -------- | -------------- | +| `abs` | absolute value | + +## Floating-point functions + +The following standard functions (defined in the standard library but excluded if you use the `no_stdlib` feature) operate on `f64` only: + +| Category | Functions | +| ---------------- | ------------------------------------------------------------ | +| Trigonometry | `sin`, `cos`, `tan`, `sinh`, `cosh`, `tanh` in degrees | +| Arc-trigonometry | `asin`, `acos`, `atan`, `asinh`, `acosh`, `atanh` in degrees | +| Square root | `sqrt` | +| Exponential | `exp` (base _e_) | +| Logarithmic | `ln` (base _e_), `log10` (base 10), `log` (any base) | +| Rounding | `floor`, `ceiling`, `round`, `int`, `fraction` | +| Tests | `is_nan`, `is_finite`, `is_infinite` | + ## Comparison operators You can compare most values of the same data type. If you compare two values of _different_ data types, the result is always `false`. @@ -664,13 +710,17 @@ You can create arrays of values, and then access them with numeric indices. The following functions (defined in the standard library but excluded if you use the `no_stdlib` feature) operate on arrays: -* `push` - inserts an element at the end -* `pop` - removes the last element and returns it (() if empty) -* `shift` - removes the first element and returns it (() if empty) -* `len` - returns the number of elements -* `pad` - pads the array with an element until a specified length -* `clear` - empties the array -* `truncate` - cuts off the array at exactly a specified length (discarding all subsequent elements) +| Function | Description | +| ---------- | ------------------------------------------------------------------------------------- | +| `push` | inserts an element at the end | +| `pop` | removes the last element and returns it (`()` if empty) | +| `shift` | removes the first element and returns it (`()` if empty) | +| `len` | returns the number of elements | +| `pad` | pads the array with an element until a specified length | +| `clear` | empties the array | +| `truncate` | cuts off the array at exactly a specified length (discarding all subsequent elements) | + +Examples: ```rust let y = [1, 2, 3]; // 3 elements @@ -813,14 +863,18 @@ record == "Bob X. Davis: age 42 ❤\n"; The following standard functions (defined in the standard library but excluded if you use the `no_stdlib` feature) operate on strings: -* `len` - returns the number of characters (not number of bytes) in the string -* `pad` - pads the string with an character until a specified number of characters -* `append` - Adds a character or a string to the end of another string -* `clear` - empties the string -* `truncate` - cuts off the string at exactly a specified number of characters -* `contains` - checks if a certain character or sub-string occurs in the string -* `replace` - replaces a substring with another -* `trim` - trims the string +| Function | Description | +| ---------- | ------------------------------------------------------------------------ | +| `len` | returns the number of characters (not number of bytes) in the string | +| `pad` | pads the string with an character until a specified number of characters | +| `append` | Adds a character or a string to the end of another string | +| `clear` | empties the string | +| `truncate` | cuts off the string at exactly a specified number of characters | +| `contains` | checks if a certain character or sub-string occurs in the string | +| `replace` | replaces a substring with another | +| `trim` | trims the string | + +Examples: ```rust let full_name == " Bob C. Davis "; diff --git a/src/builtin.rs b/src/builtin.rs index e7cfb45b..9003b13b 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -6,6 +6,7 @@ use crate::engine::{Array, Engine}; use crate::fn_register::RegisterFn; use std::fmt::{Debug, Display}; use std::ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Neg, Range, Rem, Sub}; +use std::{i32, i64, u32}; #[cfg(feature = "unchecked")] use std::ops::{Shl, Shr}; @@ -293,12 +294,41 @@ impl Engine<'_> { fn modulo_u(x: T, y: T) -> ::Output { x % y } + #[cfg(not(feature = "unchecked"))] + fn pow_i64_i64_u(x: i64, y: i64) -> Result { + if y > (u32::MAX as i64) { + return Err(EvalAltResult::ErrorArithmetic( + format!("Power overflow: {} ~ {}", x, y), + Position::none(), + )); + } + + x.checked_pow(y as u32).ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Power overflow: {} ~ {}", x, y), + Position::none(), + ) + }) + } + #[cfg(feature = "unchecked")] fn pow_i64_i64(x: i64, y: i64) -> i64 { - x.pow(y as u32) + x.powi(y as u32) } fn pow_f64_f64(x: f64, y: f64) -> f64 { x.powf(y) } + #[cfg(not(feature = "unchecked"))] + fn pow_f64_i64_u(x: f64, y: i64) -> Result { + if y > (i32::MAX as i64) { + return Err(EvalAltResult::ErrorArithmetic( + format!("Power overflow: {} ~ {}", x, y), + Position::none(), + )); + } + + Ok(x.powi(y as i32)) + } + #[cfg(feature = "unchecked")] fn pow_f64_i64(x: f64, y: i64) -> f64 { x.powi(y as i32) } @@ -361,9 +391,19 @@ impl Engine<'_> { reg_op!(self, "%", modulo_u, f32, f64); - self.register_fn("~", pow_i64_i64); self.register_fn("~", pow_f64_f64); - self.register_fn("~", pow_f64_i64); + + #[cfg(not(feature = "unchecked"))] + { + self.register_result_fn("~", pow_i64_i64_u); + self.register_result_fn("~", pow_f64_i64_u); + } + + #[cfg(feature = "unchecked")] + { + self.register_fn("~", pow_i64_i64); + self.register_fn("~", pow_f64_i64); + } #[cfg(not(feature = "unchecked"))] { @@ -427,6 +467,33 @@ impl Engine<'_> { pub(crate) fn register_stdlib(&mut self) { use crate::fn_register::RegisterDynamicFn; + // Advanced math functions + self.register_fn("sin", |x: f64| x.to_radians().sin()); + self.register_fn("cos", |x: f64| x.to_radians().cos()); + self.register_fn("tan", |x: f64| x.to_radians().tan()); + self.register_fn("sinh", |x: f64| x.to_radians().sinh()); + self.register_fn("cosh", |x: f64| x.to_radians().cosh()); + self.register_fn("tanh", |x: f64| x.to_radians().tanh()); + self.register_fn("asin", |x: f64| x.asin().to_degrees()); + self.register_fn("acos", |x: f64| x.acos().to_degrees()); + self.register_fn("atan", |x: f64| x.atan().to_degrees()); + self.register_fn("asinh", |x: f64| x.asinh().to_degrees()); + self.register_fn("acosh", |x: f64| x.acosh().to_degrees()); + self.register_fn("atanh", |x: f64| x.atanh().to_degrees()); + self.register_fn("sqrt", |x: f64| x.sqrt()); + self.register_fn("exp", |x: f64| x.exp()); + self.register_fn("ln", |x: f64| x.ln()); + self.register_fn("log", |x: f64, base: f64| x.log(base)); + self.register_fn("log10", |x: f64| x.log10()); + self.register_fn("floor", |x: f64| x.floor()); + self.register_fn("ceiling", |x: f64| x.ceil()); + self.register_fn("round", |x: f64| x.ceil()); + self.register_fn("int", |x: f64| x.trunc()); + self.register_fn("fraction", |x: f64| x.fract()); + self.register_fn("is_nan", |x: f64| x.is_nan()); + self.register_fn("is_finite", |x: f64| x.is_finite()); + self.register_fn("is_infinite", |x: f64| x.is_infinite()); + // Register conversion functions self.register_fn("to_float", |x: i8| x as f64); self.register_fn("to_float", |x: u8| x as f64); @@ -445,11 +512,38 @@ impl Engine<'_> { self.register_fn("to_int", |x: i32| x as i64); self.register_fn("to_int", |x: u32| x as i64); self.register_fn("to_int", |x: u64| x as i64); - self.register_fn("to_int", |x: f32| x as i64); - self.register_fn("to_int", |x: f64| x as i64); - self.register_fn("to_int", |ch: char| ch as i64); + #[cfg(not(feature = "unchecked"))] + { + self.register_result_fn("to_int", |x: f32| { + if x > (i64::MAX as f32) { + return Err(EvalAltResult::ErrorArithmetic( + format!("Integer overflow: to_int({})", x), + Position::none(), + )); + } + + Ok(x.trunc() as i64) + }); + self.register_result_fn("to_int", |x: f64| { + if x > (i64::MAX as f64) { + return Err(EvalAltResult::ErrorArithmetic( + format!("Integer overflow: to_int({})", x), + Position::none(), + )); + } + + Ok(x.trunc() as i64) + }); + } + + #[cfg(feature = "unchecked")] + { + self.register_fn("to_int", |x: f32| x as i64); + self.register_fn("to_int", |x: f64| x as i64); + } + // Register array utility functions fn push(list: &mut Array, item: T) { list.push(Box::new(item)); From a6899afb1150fab4eb895e570779e3dec64c9955 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 9 Mar 2020 12:53:07 +0800 Subject: [PATCH 18/27] Refine README. --- README.md | 785 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 404 insertions(+), 381 deletions(-) diff --git a/README.md b/README.md index 45371f98..66ec3a9d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# Rhai - Embedded Scripting for Rust +Rhai - Embedded Scripting for Rust +================================= Rhai is an embedded scripting language for Rust that gives you a safe and easy way to add scripting to your applications. @@ -13,7 +14,8 @@ Rhai's current feature set: **Note:** Currently, the version is 0.10.2, so the language and API's may change before they stabilize. -## Installation +Installation +------------ You can install Rhai using crates by adding this line to your dependencies: @@ -33,7 +35,8 @@ to use the latest version. Beware that in order to use pre-releases (alpha and beta) you need to specify the exact version in your `Cargo.toml`. -## Optional Features +Optional features +----------------- ### `debug_msgs` @@ -47,24 +50,28 @@ Exclude the standard library of utility functions in the build, and only include Exclude arithmetic checking in the standard library. Beware that a bad script may panic the entire system! -## Related +Related +------- Other cool projects to check out: * [ChaiScript](http://chaiscript.com/) - A strong inspiration for Rhai. An embedded scripting language for C++ that I helped created many moons ago, now being lead by my cousin. * You can also check out the list of [scripting languages for Rust](https://github.com/rust-unofficial/awesome-rust#scripting) on [awesome-rust](https://github.com/rust-unofficial/awesome-rust) -## Examples +Examples +-------- The repository contains several examples in the `examples` folder: -* `arrays_and_structs` demonstrates registering a new type to Rhai and the usage of arrays on it -* `custom_types_and_methods` shows how to register a type and methods for it -* `hello` simple example that evaluates an expression and prints the result -* `reuse_scope` evaluates two pieces of code in separate runs, but using a common scope -* `rhai_runner` runs each filename passed to it as a Rhai script -* `simple_fn` shows how to register a Rust function to a Rhai engine -* `repl` a simple REPL, see source code for what it can do at the moment +| Example | Description | +| -------------------------- | ------------------------------------------------------------------------- | +| `arrays_and_structs` | demonstrates registering a new type to Rhai and the usage of arrays on it | +| `custom_types_and_methods` | shows how to register a type and methods for it | +| `hello` | simple example that evaluates an expression and prints the result | +| `reuse_scope` | evaluates two pieces of code in separate runs, but using a common scope | +| `rhai_runner` | runs each filename passed to it as a Rhai script | +| `simple_fn` | shows how to register a Rust function to a Rhai engine | +| `repl` | a simple REPL, see source code for what it can do at the moment | Examples can be run with the following command: @@ -72,25 +79,28 @@ Examples can be run with the following command: cargo run --example name ``` -## Example Scripts +Example Scripts +--------------- We also have a few examples scripts that showcase Rhai's features, all stored in the `scripts` folder: -* `array.rhai` - arrays in Rhai -* `assignment.rhai` - variable declarations -* `comments.rhai` - just comments -* `for1.rhai` - for loops -* `function_decl1.rhai` - a function without parameters -* `function_decl2.rhai` - a function with two parameters -* `function_decl3.rhai` - a function with many parameters -* `if1.rhai` - if example -* `loop.rhai` - endless loop in Rhai, this example emulates a do..while cycle -* `op1.rhai` - just a simple addition -* `op2.rhai` - simple addition and multiplication -* `op3.rhai` - change evaluation order with parenthesis -* `speed_test.rhai` - a simple program to measure the speed of Rhai's interpreter -* `string.rhai`- string operations -* `while.rhai` - while loop +| Script | Description | +| --------------------- | ------------------------------------------------------------- | +| `array.rhai` | arrays in Rhai | +| `assignment.rhai` | variable declarations | +| `comments.rhai` | just comments | +| `for1.rhai` | for loops | +| `function_decl1.rhai` | a function without parameters | +| `function_decl2.rhai` | a function with two parameters | +| `function_decl3.rhai` | a function with many parameters | +| `if1.rhai` | if example | +| `loop.rhai` | endless loop in Rhai, this example emulates a do..while cycle | +| `op1.rhai` | just a simple addition | +| `op2.rhai` | simple addition and multiplication | +| `op3.rhai` | change evaluation order with parenthesis | +| `speed_test.rhai` | a simple program to measure the speed of Rhai's interpreter | +| `string.rhai` | string operations | +| `while.rhai` | while loop | To run the scripts, you can either make your own tiny program, or make use of the `rhai_runner` example program: @@ -99,7 +109,8 @@ example program: cargo run --example rhai_runner scripts/any_script.rhai ``` -# Hello world +Hello world +----------- To get going with Rhai, you create an instance of the scripting engine and then run eval. @@ -153,7 +164,8 @@ let ast = Engine::compile("fn hello(x, y) { x.len() + y }")?; let result: i64 = engine.call_fn("hello", &ast, (&mut String::from("abc"), &mut 123_i64))?; ``` -# Values and types +Values and types +---------------- The following primitive types are supported natively: @@ -166,7 +178,8 @@ The following primitive types are supported natively: | Array | `rhai::Array` | | Dynamic (i.e. can be anything) | `rhai::Dynamic` | -# Value conversions +Value conversions +----------------- All types are treated strictly separate by Rhai, meaning that `i32` and `i64` and `u32` are completely different; you cannot even add them together. @@ -193,7 +206,8 @@ if z.type_of() == "string" { } ``` -# Working with functions +Working with functions +---------------------- Rhai's scripting engine is very lightweight. It gets its ability from the functions in your program. To call these functions, you need to register them with the scripting engine. @@ -242,7 +256,8 @@ fn decide(yes_no: bool) -> Dynamic { } ``` -# Generic functions +Generic functions +----------------- Generic functions can be used in Rhai, but you'll need to register separate instances for each concrete type: @@ -266,7 +281,8 @@ fn main() { You can also see in this example how you can register multiple functions (or in this case multiple instances of the same function) to the same name in script. This gives you a way to overload functions and call the correct one, based on the types of the arguments, from your script. -# Fallible functions +Fallible functions +------------------ If your function is _fallible_ (i.e. it returns a `Result<_, Error>`), you can register it with `register_result_fn` (using the `RegisterResultFn` trait). @@ -298,7 +314,8 @@ fn main() { } ``` -# Overriding built-in functions +Overriding built-in functions +---------------------------- Any similarly-named function defined in a script overrides any built-in function. @@ -311,7 +328,8 @@ fn to_int(num) { print(to_int(123)); // what will happen? ``` -# Custom types and methods +Custom types and methods +----------------------- Here's an more complete example of working with Rust. First the example, then we'll break it into parts: @@ -414,7 +432,8 @@ print(x.type_of()); // prints "foo::bar::TestStruct" If you use `register_type_with_name` to register the custom type with a special pretty-print name, `type_of` will return that instead. -# Getters and setters +Getters and setters +------------------- Similarly, you can work with members of your custom types. This works by registering a 'get' or a 'set' function for working with your struct. @@ -452,24 +471,8 @@ if let Ok(result) = engine.eval::("let a = new_ts(); a.x = 500; a.x") { } ``` -### WARNING: Gotcha's with Getters - -When you _get_ a property, the value is cloned. Any update to it downstream will **NOT** be reflected back to the custom type. - -This can introduce subtle bugs. For example: - -```rust -fn change(s) { - s = 42; -} - -let a = new_ts(); -a.x = 500; -a.x.change(); // Only a COPY of 'a.x' is changed. 'a.x' is NOT changed. -a.x == 500; -``` - -# Initializing and maintaining state +Initializing and maintaining state +--------------------------------- By default, Rhai treats each engine invocation as a fresh one, persisting only the functions that have been defined but no top-level state. This gives each one a fairly clean starting place. Sometimes, though, you want to continue using the same top-level state from one invocation to the next. @@ -506,32 +509,48 @@ fn main() { } ``` -# Rhai Language guide +Rhai Language guide +=================== -## Variables +Comments +-------- -Variables in `Rhai` follow normal naming rules: +```rust +let /* intruder comment */ name = "Bob"; +// This is a very important comment +/* This comment spans + multiple lines, so it + only makes sense that + it is even more important */ -* Must start with an ASCII letter -* Must contain only ASCII letters, digits and `_` underscores +/* Fear not, Rhai satisfies all your nesting + needs with nested comments: + /*/*/*/*/**/*/*/*/*/ +*/ +``` -Example: +Variables +--------- + +Variables in `Rhai` follow normal naming rules (i.e. must contain only ASCII letters, digits and '`_`' underscores). ```rust let x = 3; ``` -## Numbers +Numbers +------- -| Format | Type | -| ---------------- | ------------------------------------------------------ | -| `123_345`, `-42` | `i64` in decimal, '`_`' separator can be used anywhere | -| `0o07_76` | `i64` in octal, '`_`' separator can be used anywhere | -| `0xabcd_ef` | `i64` in hex, '`_`' separator can be used anywhere | -| `0b0101_1001` | `i64` in binary, '`_`' separator can be used anywhere | -| `123_456.789` | `f64`, '`_`' separator can be used anywhere | +| Format | Type | +| ---------------- | ---------------------------------------------- | +| `123_345`, `-42` | `i64` in decimal, '`_`' separators are ignored | +| `0o07_76` | `i64` in octal, '`_`' separators are ignored | +| `0xabcd_ef` | `i64` in hex, '`_`' separators are ignored | +| `0b0101_1001` | `i64` in binary, '`_`' separators are ignored | +| `123_456.789` | `f64`, '`_`' separators are ignored | -## Numeric operators +Numeric operators +----------------- ```rust let x = (1 + 2) * (6 - 4) / 2; // arithmetic @@ -542,17 +561,30 @@ let right_shifted = 42 >> 3; // right shift let bit_op = 42 | 99; // bit masking ``` -## Numeric functions +Unary operators +--------------- -The following standard functions (defined in the standard library but excluded if you use the `no_stdlib` feature) operate on `i8`, `i16`, `i32`, `i64`, `f32` and `f64` only: +```rust +let number = -5; +number = -5 - +5; +let booly = !true; +``` -| Category | Functions | -| -------- | -------------- | -| `abs` | absolute value | +Numeric functions +----------------- -## Floating-point functions +The following standard functions (defined in the standard library but excluded if `no_stdlib`) operate on `i8`, `i16`, `i32`, `i64`, `f32` and `f64` only: -The following standard functions (defined in the standard library but excluded if you use the `no_stdlib` feature) operate on `f64` only: +| Function | Description | +| ---------- | ----------------------------------- | +| `abs` | absolute value | +| `to_int` | converts an `f32` or `f64` to `i64` | +| `to_float` | converts an integer type to `f64` | + +Floating-point functions +------------------------ + +The following standard functions (defined in the standard library but excluded if `no_stdlib`) operate on `f64` only: | Category | Functions | | ---------------- | ------------------------------------------------------------ | @@ -564,151 +596,94 @@ The following standard functions (defined in the standard library but excluded i | Rounding | `floor`, `ceiling`, `round`, `int`, `fraction` | | Tests | `is_nan`, `is_finite`, `is_infinite` | -## Comparison operators - -You can compare most values of the same data type. If you compare two values of _different_ data types, the result is always `false`. +Strings and Chars +----------------- ```rust -42 == 42; // true -42 > 42; // false -"hello" > "foo"; // true -"42" == 42; // false -42 == 42.0; // false - i64 is different from f64 +let name = "Bob"; +let middle_initial = 'C'; +let last = "Davis"; + +let full_name = name + " " + middle_initial + ". " + last; +full_name == "Bob C. Davis"; + +// String building with different types +let age = 42; +let record = full_name + ": age " + age; +record == "Bob C. Davis: age 42"; + +// Strings can be indexed to get a character +let c = record[4]; +c == 'C'; + +ts.s = record; + +let c = ts.s[4]; +c == 'C'; + +let c = "foo"[0]; +c == 'f'; + +let c = ("foo" + "bar")[5]; +c == 'r'; + +// Escape sequences in strings +record += " \u2764\n"; // escape sequence of '❤' in Unicode +record == "Bob C. Davis: age 42 ❤\n"; // '\n' = new-line + +// Unlike Rust, Rhai strings can be modified +record[4] = '\x58'; // 0x58 = 'X' +record == "Bob X. Davis: age 42 ❤\n"; ``` -## Boolean operators +The following standard functions (defined in the standard library but excluded if `no_stdlib`) operate on strings: -Double boolean operators `&&` and `||` _short-circuit_, meaning that the second operand will not be evaluated if the first one already proves the condition wrong. +| Function | Description | +| ---------- | ------------------------------------------------------------------------ | +| `len` | returns the number of characters (not number of bytes) in the string | +| `pad` | pads the string with an character until a specified number of characters | +| `append` | Adds a character or a string to the end of another string | +| `clear` | empties the string | +| `truncate` | cuts off the string at exactly a specified number of characters | +| `contains` | checks if a certain character or sub-string occurs in the string | +| `replace` | replaces a substring with another | +| `trim` | trims the string | -Single boolean operators `&` and `|` always evaluate both operands. +Examples: ```rust -this() || that(); // that() is not evaluated if this() is true -this() && that(); // that() is not evaluated if this() is false +let full_name == " Bob C. Davis "; +full_name.len() == 14; -this() | that(); // both this() and that() are evaluated -this() & that(); // both this() and that() are evaluated +full_name.trim(); +full_name.len() == 12; +full_name == "Bob C. Davis"; + +full_name.pad(15, '$'); +full_name.len() == 15; +full_name == "Bob C. Davis$$$"; + +full_name.truncate(6); +full_name.len() == 6; +full_name == "Bob C."; + +full_name.replace("Bob", "John"); +full_name.len() == 7; +full_name = "John C."; + +full_name.contains('C') == true; +full_name.contains("John") == true; + +full_name.clear(); +full_name.len() == 0; ``` -## If - -```rust -if true { - print("It's true!"); -} else if true { - print("It's true again!"); -} else { - print("It's false!"); -} -``` - -## While - -```rust -let x = 10; - -while x > 0 { - print(x); - if x == 5 { break; } - x = x - 1; -} -``` - -## Loop - -```rust -let x = 10; - -loop { - print(x); - x = x - 1; - if x == 0 { break; } -} -``` - -## Functions - -Rhai supports defining functions in script: - -```rust -fn add(x, y) { - return x + y; -} - -print(add(2, 3)); -``` - -Just like in Rust, you can also use an implicit return. - -```rust -fn add(x, y) { - x + y -} - -print(add(2, 3)); -``` - -Remember that functions defined in script always take `Dynamic` arguments (i.e. the arguments can be of any type). - -Arguments are passed by value, so all functions are _pure_ (i.e. they never modify their arguments). - -Furthermore, functions can only be defined at the top level, never inside a block or another function. - -```rust -// Top level is OK -fn add(x, y) { - x + y -} - -// The following will not compile -fn do_addition(x) { - fn add_y(n) { // functions cannot be defined inside another function - n + y - } - - add_y(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 +Arrays +------ You can create arrays of values, and then access them with numeric indices. -The following functions (defined in the standard library but excluded if you use the `no_stdlib` feature) operate on arrays: +The following functions (defined in the standard library but excluded if `no_stdlib`) operate on arrays: | Function | Description | | ---------- | ------------------------------------------------------------------------------------- | @@ -780,190 +755,46 @@ engine.register_fn("push", The type of a Rhai array is `rhai::Array`. `type_of()` returns `"array"`. -## For loops +Comparison operators +-------------------- + +You can compare most values of the same data type. If you compare two values of _different_ data types, the result is always `false`. ```rust -let array = [1, 3, 5, 7, 9, 42]; - -for x in array { - print(x); - if x == 42 { break; } -} - -// The range function allows iterating from first..last-1 -for x in range(0,50) { - print(x); - if x == 42 { break; } -} +42 == 42; // true +42 > 42; // false +"hello" > "foo"; // true +"42" == 42; // false +42 == 42.0; // false - i64 is different from f64 ``` -## Members and methods +Boolean operators +----------------- + +Double boolean operators `&&` and `||` _short-circuit_, meaning that the second operand will not be evaluated if the first one already proves the condition wrong. + +Single boolean operators `&` and `|` always evaluate both operands. ```rust -let a = new_ts(); -a.x = 500; -a.update(); +this() || that(); // that() is not evaluated if this() is true +this() && that(); // that() is not evaluated if this() is false + +this() | that(); // both this() and that() are evaluated +this() & that(); // both this() and that() are evaluated ``` -## Numbers - -```rust -let x = 123; // i64 -let x = 123.4; // f64 -let x = 123_456_789; // separators can be put anywhere inside the number - -let x = 0x12abcd; // i64 in hex -let x = 0o777; // i64 in oct -let x = 0b1010_1111; // i64 in binary -``` - -Conversion functions (defined in the standard library but excluded if you use the `no_stdlib` feature): - -* `to_int` - converts an `f32` or `f64` to `i64` -* `to_float` - converts an integer type to `f64` - -## Strings and Chars - -```rust -let name = "Bob"; -let middle_initial = 'C'; -let last = "Davis"; - -let full_name = name + " " + middle_initial + ". " + last; -full_name == "Bob C. Davis"; - -// String building with different types (not available if 'no_stdlib' features is used) -let age = 42; -let record = full_name + ": age " + age; -record == "Bob C. Davis: age 42"; - -// Strings can be indexed to get a character -let c = record[4]; -c == 'C'; - -ts.s = record; - -let c = ts.s[4]; -c == 'C'; - -let c = "foo"[0]; -c == 'f'; - -let c = ("foo" + "bar")[5]; -c == 'r'; - -// Escape sequences in strings -record += " \u2764\n"; // escape sequence of '❤' in Unicode -record == "Bob C. Davis: age 42 ❤\n"; // '\n' = new-line - -// Unlike Rust, Rhai strings can be modified -record[4] = '\x58'; // 0x58 = 'X' -record == "Bob X. Davis: age 42 ❤\n"; -``` - -The following standard functions (defined in the standard library but excluded if you use the `no_stdlib` feature) operate on strings: - -| Function | Description | -| ---------- | ------------------------------------------------------------------------ | -| `len` | returns the number of characters (not number of bytes) in the string | -| `pad` | pads the string with an character until a specified number of characters | -| `append` | Adds a character or a string to the end of another string | -| `clear` | empties the string | -| `truncate` | cuts off the string at exactly a specified number of characters | -| `contains` | checks if a certain character or sub-string occurs in the string | -| `replace` | replaces a substring with another | -| `trim` | trims the string | - -Examples: - -```rust -let full_name == " Bob C. Davis "; -full_name.len() == 14; - -full_name.trim(); -full_name.len() == 12; -full_name == "Bob C. Davis"; - -full_name.pad(15, '$'); -full_name.len() == 15; -full_name == "Bob C. Davis$$$"; - -full_name.truncate(6); -full_name.len() == 6; -full_name == "Bob C."; - -full_name.replace("Bob", "John"); -full_name.len() == 7; -full_name = "John C."; - -full_name.contains('C') == true; -full_name.contains("John") == true; - -full_name.clear(); -full_name.len() == 0; -``` - -## Print and Debug - -```rust -print("hello"); // prints hello to stdout -print(1 + 2 + 3); // prints 6 to stdout -print("hello" + 42); // prints hello42 to stdout -debug("world!"); // prints "world!" to stdout using debug formatting -``` - -### Overriding Print and Debug with Callback functions - -```rust -// Any function that takes a &str argument can be used to override print and debug -engine.on_print(|x| println!("hello: {}", x)); -engine.on_debug(|x| println!("DEBUG: {}", x)); - -// Redirect logging output to somewhere else -let mut log: Vec = Vec::new(); -engine.on_print(|x| log.push(format!("log: {}", x))); -engine.on_debug(|x| log.push(format!("DEBUG: {}", x))); - : - eval script - : -println!("{:?}", log); // 'log' captures all the 'print' and 'debug' results. -``` - -## Comments - -```rust -let /* intruder comment */ name = "Bob"; -// This is a very important comment -/* This comment spans - multiple lines, so it - only makes sense that - it is even more important */ - -/* Fear not, Rhai satisfies all your nesting - needs with nested comments: - /*/*/*/*/**/*/*/*/*/ -*/ -``` - -## Unary operators - -```rust -let number = -5; -number = -5 - +5; -let booly = !true; -``` - -## Compound assignment operators +Compound assignment operators +---------------------------- ```rust let number = 5; -number += 4; -number -= 3; -number *= 2; -number /= 1; -number %= 3; -number <<= 2; -number >>= 1; +number += 4; // number = number + 4 +number -= 3; // number = number - 3 +number *= 2; // number = number * 2 +number /= 1; // number = number / 1 +number %= 3; // number = number % 3 +number <<= 2; // number = number << 2 +number >>= 1; // number = number >> 1 ``` The `+=` operator can also be used to build strings: @@ -975,3 +806,195 @@ my_str += 12345; my_str == "abcABC12345" ``` + +If +-- + +```rust +if true { + print("It's true!"); +} else if true { + print("It's true again!"); +} else { + print("It's false!"); +} +``` + +While +----- + +```rust +let x = 10; + +while x > 0 { + print(x); + if x == 5 { break; } + x = x - 1; +} +``` + +Loop +---- + +```rust +let x = 10; + +loop { + print(x); + x = x - 1; + if x == 0 { break; } +} +``` + +For +--- + +```rust +let array = [1, 3, 5, 7, 9, 42]; + +// Iterate through array +for x in array { + print(x); + if x == 42 { break; } +} + +// The 'range' function allows iterating from first..last-1 +for x in range(0, 50) { + print(x); + if x == 42 { break; } +} +``` + +Return +------ + +```rust +return; // equivalent to return (); + +return 123 + 456; +``` + +Errors and Exceptions +--------------------- + +```rust +if some_bad_condition_has_happened { + 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)" +``` + +Functions +--------- + +Rhai supports defining functions in script: + +```rust +fn add(x, y) { + return x + y; +} + +print(add(2, 3)); +``` + +Just like in Rust, you can also use an implicit return. + +```rust +fn add(x, y) { + x + y +} + +print(add(2, 3)); +``` + +Remember that functions defined in script always take `Dynamic` arguments (i.e. the arguments can be of any type). + +However, all arguments are passed by _value_, so all functions are _pure_ (i.e. they never modify their arguments). +Any update to an argument will **not** be reflected back to the caller. This can introduce subtle bugs, if you are not careful. + +```rust +fn change(s) { + s = 42; // only a COPY of 'x' is changed +} + +let x = 500; +x.change(); +x == 500; // 'x' is NOT changed! +``` + +Furthermore, functions can only be defined at the top level, never inside a block or another function. + +```rust +// Top level is OK +fn add(x, y) { + x + y +} + +// The following will not compile +fn do_addition(x) { + fn add_y(n) { // functions cannot be defined inside another function + n + y + } + + add_y(x) +} +``` + +Members and methods +------------------- + +```rust +let a = new_ts(); +a.x = 500; +a.update(); +``` + +`print` and `debug` +------------------- + +```rust +print("hello"); // prints hello to stdout +print(1 + 2 + 3); // prints 6 to stdout +print("hello" + 42); // prints hello42 to stdout +debug("world!"); // prints "world!" to stdout using debug formatting +``` + +### Overriding `print` and `debug` with callback functions + +```rust +// Any function or closure that takes an &str argument can be used to override print and debug +engine.on_print(|x| println!("hello: {}", x)); +engine.on_debug(|x| println!("DEBUG: {}", x)); + +// Example: quick-'n-dirty logging +let mut log: Vec = Vec::new(); + +// Redirect print/debug output to 'log' +engine.on_print(|s| log.push(format!("entry: {}", s))); +engine.on_debug(|s| log.push(format!("DEBUG: {}", s))); + +// Evalulate script +engine.eval::<()>(script)?; + +// 'log' captures all the 'print' and 'debug' output +for entry in log { + println!("{}", entry); +} +``` From 1ca9db4379af8d5121c7d1c88e5b380ef6255011 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 9 Mar 2020 14:15:32 +0800 Subject: [PATCH 19/27] Fix i64.powi() to i64.pos() --- src/builtin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/builtin.rs b/src/builtin.rs index 9003b13b..546d680f 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -312,7 +312,7 @@ impl Engine<'_> { } #[cfg(feature = "unchecked")] fn pow_i64_i64(x: i64, y: i64) -> i64 { - x.powi(y as u32) + x.pow(y as u32) } fn pow_f64_f64(x: f64, y: f64) -> f64 { x.powf(y) From 63482d5a79e99b20c99a59b795be1cb2843c20e5 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 9 Mar 2020 16:54:43 +0800 Subject: [PATCH 20/27] Use ? operator for examples. --- README.md | 117 ++++++++++++++++++++++------------------ examples/hello.rs | 12 +++-- examples/reuse_scope.rs | 16 +++--- examples/simple_fn.rs | 12 +++-- 4 files changed, 87 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 66ec3a9d..a135bcae 100644 --- a/README.md +++ b/README.md @@ -38,17 +38,11 @@ Beware that in order to use pre-releases (alpha and beta) you need to specify th Optional features ----------------- -### `debug_msgs` - -Print debug messages to stdout (using `println!`) related to function registrations and function calls. - -### `no_stdlib` - -Exclude the standard library of utility functions in the build, and only include the minimum necessary functionalities. - -### `unchecked` - -Exclude arithmetic checking in the standard library. Beware that a bad script may panic the entire system! +| Feature | Description | +| ------------ | ----------------------------------------------------------------------------------------------------------------------- | +| `debug_msgs` | Print debug messages to stdout (using `println!`) related to function registrations and function calls. | +| `no_stdlib` | Exclude the standard library of utility functions in the build, and only include the minimum necessary functionalities. | +| `unchecked` | Exclude arithmetic checking in the standard library. Beware that a bad script may panic the entire system! | Related ------- @@ -115,40 +109,48 @@ Hello world To get going with Rhai, you create an instance of the scripting engine and then run eval. ```rust -use rhai::Engine; +use rhai::{Engine, EvalAltResult}; -fn main() { +fn main() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); - if let Ok(result) = engine.eval::("40 + 2") { - println!("Answer: {}", result); // prints 42 - } + let result = engine.eval::("40 + 2")?; + + println!("Answer: {}", result); // prints 42 } ``` You can also evaluate a script file: ```rust -if let Ok(result) = engine.eval_file::("hello_world.rhai") { ... } +let result = engine.eval_file::("hello_world.rhai")?; ``` If you want to repeatedly evaluate a script, you can _compile_ it first into an AST (abstract syntax tree) form: ```rust +use rhai::Engine; + +let mut engine = Engine::new(); + // Compile to an AST and store it for later evaluations -let ast = Engine::compile("40 + 2").unwrap(); +let ast = engine.compile("40 + 2")?; for _ in 0..42 { - if let Ok(result) = engine.eval_ast::(&ast) { - println!("Answer: {}", result); // prints 42 - } + let result = engine.eval_ast::(&ast)?; + + println!("Answer: {}", result); // prints 42 } ``` Compiling a script file is also supported: ```rust -let ast = Engine::compile_file("hello_world.rhai").unwrap(); +use rhai::Engine; + +let mut engine = Engine::new(); + +let ast = engine.compile_file("hello_world.rhai").unwrap(); ``` Rhai also allows you to work _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust. @@ -156,8 +158,12 @@ You do this via `call_fn`, which takes a compiled AST (output from `compile`) an function call arguments: ```rust +use rhai::Engine; + +let mut engine = Engine::new(); + // Define a function in a script and compile to AST -let ast = Engine::compile("fn hello(x, y) { x.len() + y }")?; +let ast = engine.compile("fn hello(x, y) { x.len() + y }")?; // Evaluate the function in the AST, passing arguments into the script as a tuple // (beware, arguments must be of the correct types because Rhai does not have built-in type conversions) @@ -212,7 +218,7 @@ Working with functions Rhai's scripting engine is very lightweight. It gets its ability from the functions in your program. To call these functions, you need to register them with the scripting engine. ```rust -use rhai::Engine; +use rhai::{Engine, EvalAltResult}; use rhai::RegisterFn; // include the `RegisterFn` trait to use `register_fn` use rhai::{Dynamic, RegisterDynamicFn}; // include the `RegisterDynamicFn` trait to use `register_dynamic_fn` @@ -226,21 +232,23 @@ fn get_an_any() -> Dynamic { Box::new(42_i64) } -fn main() { +fn main() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); engine.register_fn("add", add); - if let Ok(result) = engine.eval::("add(40, 2)") { - println!("Answer: {}", result); // prints 42 - } + let result = engine.eval::("add(40, 2)")?; + + println!("Answer: {}", result); // prints 42 // Functions that return Dynamic values must use register_dynamic_fn() engine.register_dynamic_fn("get_an_any", get_an_any); - if let Ok(result) = engine.eval::("get_an_any()") { - println!("Answer: {}", result); // prints 42 - } + let result = engine.eval::("get_an_any()")?; + + println!("Answer: {}", result); // prints 42 + + Ok(()) } ``` @@ -334,7 +342,8 @@ Custom types and methods Here's an more complete example of working with Rust. First the example, then we'll break it into parts: ```rust -use rhai::{Engine, RegisterFn}; +use rhai::{Engine, EvalAltResult}; +use rhai::RegisterFn; #[derive(Clone)] struct TestStruct { @@ -351,7 +360,7 @@ impl TestStruct { } } -fn main() { +fn main() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); engine.register_type::(); @@ -359,9 +368,11 @@ fn main() { engine.register_fn("update", TestStruct::update); engine.register_fn("new_ts", TestStruct::new); - if let Ok(result) = engine.eval::("let x = new_ts(); x.update(); x") { - println!("result: {}", result.x); // prints 1001 - } + let result = engine.eval::("let x = new_ts(); x.update(); x")?; + + println!("result: {}", result.x); // prints 1001 + + Ok(()) } ``` @@ -404,9 +415,9 @@ engine.register_fn("new_ts", TestStruct::new); Finally, we call our script. The script can see the function and method we registered earlier. We need to get the result back out from script land just as before, this time casting to our custom struct type. ```rust -if let Ok(result) = engine.eval::("let x = new_ts(); x.update(); x") { - println!("result: {}", result.x); // prints 1001 -} +let result = engine.eval::("let x = new_ts(); x.update(); x")?; + +println!("result: {}", result.x); // prints 1001 ``` In fact, any function with a first argument (either by copy or via a `&mut` reference) can be used as a method-call on that type because internally they are the same thing: methods on a type is implemented as a functions taking an first argument. @@ -418,9 +429,9 @@ fn foo(ts: &mut TestStruct) -> i64 { engine.register_fn("foo", foo); -if let Ok(result) = engine.eval::("let x = new_ts(); x.foo()") { - println!("result: {}", result); // prints 1 -} +let result = engine.eval::("let x = new_ts(); x.foo()")?; + +println!("result: {}", result); // prints 1 ``` `type_of` works fine with custom types and returns the name of the type: @@ -466,9 +477,9 @@ engine.register_type::(); engine.register_get_set("x", TestStruct::get_x, TestStruct::set_x); engine.register_fn("new_ts", TestStruct::new); -if let Ok(result) = engine.eval::("let a = new_ts(); a.x = 500; a.x") { - println!("result: {}", result); -} +let result = engine.eval::("let a = new_ts(); a.x = 500; a.x")?; + +println!("result: {}", result); ``` Initializing and maintaining state @@ -479,9 +490,9 @@ By default, Rhai treats each engine invocation as a fresh one, persisting only t In this example, we first create a state with a few initialized variables, then thread the same state through multiple invocations: ```rust -use rhai::{Engine, Scope}; +use rhai::{Engine, Scope, EvalAltResult}; -fn main() { +fn main() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); // First create the state @@ -497,15 +508,17 @@ fn main() { engine.eval_with_scope::<()>(&mut scope, r" let x = 4 + 5 - y + z; y = 1; - ").expect("y and z not found?"); + ")?; // Second invocation using the same state - if let Ok(result) = engine.eval_with_scope::(&mut scope, "x") { - println!("result: {}", result); // should print 966 - } + let result = engine.eval_with_scope::(&mut scope, "x")?; + + println!("result: {}", result); // should print 966 // Variable y is changed in the script - assert_eq!(scope.get_value::("y").unwrap(), 1); + assert_eq!(scope.get_value::("y")?, 1); + + Ok(()) } ``` diff --git a/examples/hello.rs b/examples/hello.rs index cb889fe5..590074e7 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -1,9 +1,11 @@ -use rhai::Engine; +use rhai::{Engine, EvalAltResult}; -fn main() { +fn main() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); - if let Ok(result) = engine.eval::("40 + 2") { - println!("Answer: {}", result); // prints 42 - } + let result = engine.eval::("40 + 2")?; + + println!("Answer: {}", result); // prints 42 + + Ok(()) } diff --git a/examples/reuse_scope.rs b/examples/reuse_scope.rs index 071eb9f3..a6509ffe 100644 --- a/examples/reuse_scope.rs +++ b/examples/reuse_scope.rs @@ -1,14 +1,14 @@ -use rhai::{Engine, Scope}; +use rhai::{Engine, EvalAltResult, Scope}; -fn main() { +fn main() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); let mut scope = Scope::new(); - assert!(engine - .eval_with_scope::<()>(&mut scope, "let x = 4 + 5") - .is_ok()); + engine.eval_with_scope::<()>(&mut scope, "let x = 4 + 5")?; - if let Ok(result) = engine.eval_with_scope::(&mut scope, "x") { - println!("result: {}", result); - } + let result = engine.eval_with_scope::(&mut scope, "x")?; + + println!("result: {}", result); + + Ok(()) } diff --git a/examples/simple_fn.rs b/examples/simple_fn.rs index 4be5e5b6..994eb7ec 100644 --- a/examples/simple_fn.rs +++ b/examples/simple_fn.rs @@ -1,6 +1,6 @@ -use rhai::{Engine, RegisterFn}; +use rhai::{Engine, EvalAltResult, RegisterFn}; -fn main() { +fn main() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); fn add(x: i64, y: i64) -> i64 { @@ -9,7 +9,9 @@ fn main() { engine.register_fn("add", add); - if let Ok(result) = engine.eval::("add(40, 2)") { - println!("Answer: {}", result); // prints 42 - } + let result = engine.eval::("add(40, 2)")?; + + println!("Answer: {}", result); // prints 42 + + Ok(()) } From 5b5fd162bed99d9e5c066d55dd49d16fddcfde6c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 9 Mar 2020 21:09:53 +0800 Subject: [PATCH 21/27] Use ? operator in tests. --- examples/custom_types_and_methods.rs | 12 +++++++----- src/fn_register.rs | 29 ++++++++++++++++++---------- src/lib.rs | 22 ++++++++++++++------- src/scope.rs | 8 ++++++-- tests/engine.rs | 2 +- tests/var_scope.rs | 6 +++--- 6 files changed, 51 insertions(+), 28 deletions(-) diff --git a/examples/custom_types_and_methods.rs b/examples/custom_types_and_methods.rs index 85cb2b60..4471cadf 100644 --- a/examples/custom_types_and_methods.rs +++ b/examples/custom_types_and_methods.rs @@ -1,4 +1,4 @@ -use rhai::{Engine, RegisterFn}; +use rhai::{Engine, EvalAltResult, RegisterFn}; #[derive(Clone)] struct TestStruct { @@ -15,7 +15,7 @@ impl TestStruct { } } -fn main() { +fn main() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); engine.register_type::(); @@ -23,7 +23,9 @@ fn main() { engine.register_fn("update", TestStruct::update); engine.register_fn("new_ts", TestStruct::new); - if let Ok(result) = engine.eval::("let x = new_ts(); x.update(); x") { - println!("result: {}", result.x); // prints 1001 - } + let result = engine.eval::("let x = new_ts(); x.update(); x")?; + + println!("result: {}", result.x); // prints 1001 + + Ok(()) } diff --git a/src/fn_register.rs b/src/fn_register.rs index a213474a..9af9ab82 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -11,6 +11,7 @@ use std::any::TypeId; /// # Example /// /// ```rust +/// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::{Engine, RegisterFn}; /// /// // Normal function @@ -23,9 +24,11 @@ use std::any::TypeId; /// // You must use the trait rhai::RegisterFn to get this method. /// engine.register_fn("add", add); /// -/// if let Ok(result) = engine.eval::("add(40, 2)") { -/// println!("Answer: {}", result); // prints 42 -/// } +/// let result = engine.eval::("add(40, 2)")?; +/// +/// println!("Answer: {}", result); // prints 42 +/// # Ok(()) +/// # } /// ``` pub trait RegisterFn { /// Register a custom function with the `Engine`. @@ -37,7 +40,8 @@ pub trait RegisterFn { /// # Example /// /// ```rust -/// use rhai::{Engine, RegisterDynamicFn, Dynamic}; +/// # fn main() -> Result<(), rhai::EvalAltResult> { +/// use rhai::{Engine, Dynamic, RegisterDynamicFn}; /// /// // Function that returns a Dynamic value /// fn get_an_any(x: i64) -> Dynamic { @@ -49,9 +53,11 @@ pub trait RegisterFn { /// // You must use the trait rhai::RegisterDynamicFn to get this method. /// engine.register_dynamic_fn("get_an_any", get_an_any); /// -/// if let Ok(result) = engine.eval::("get_an_any(42)") { -/// println!("Answer: {}", result); // prints 42 -/// } +/// let result = engine.eval::("get_an_any(42)")?; +/// +/// println!("Answer: {}", result); // prints 42 +/// # Ok(()) +/// # } /// ``` pub trait RegisterDynamicFn { /// Register a custom function returning `Dynamic` values with the `Engine`. @@ -63,6 +69,7 @@ pub trait RegisterDynamicFn { /// # Example /// /// ```rust +/// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::{Engine, RegisterFn}; /// /// // Normal function @@ -75,9 +82,11 @@ pub trait RegisterDynamicFn { /// // You must use the trait rhai::RegisterFn to get this method. /// engine.register_fn("add", add); /// -/// if let Ok(result) = engine.eval::("add(40, 2)") { -/// println!("Answer: {}", result); // prints 42 -/// } +/// let result = engine.eval::("add(40, 2)")?; +/// +/// println!("Answer: {}", result); // prints 42 +/// # Ok(()) +/// # } /// ``` pub trait RegisterResultFn { /// Register a custom function with the `Engine`. diff --git a/src/lib.rs b/src/lib.rs index 541963c1..56df1ae2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,15 +17,22 @@ //! And the Rust part: //! //! ```rust,no_run -//! use rhai::{Engine, RegisterFn}; +//! use rhai::{Engine, EvalAltResult, RegisterFn}; //! -//! fn compute_something(x: i64) -> bool { -//! (x % 40) == 0 +//! fn main() -> Result<(), EvalAltResult> +//! { +//! fn compute_something(x: i64) -> bool { +//! (x % 40) == 0 +//! } +//! +//! let mut engine = Engine::new(); +//! +//! engine.register_fn("compute_something", compute_something); +//! +//! assert_eq!(engine.eval_file::("my_script.rhai")?, true); +//! +//! Ok(()) //! } -//! -//! let mut engine = Engine::new(); -//! engine.register_fn("compute_something", compute_something); -//! assert_eq!(engine.eval_file::("my_script.rhai").unwrap(), true); //! ``` //! //! [Check out the README on GitHub for more information!](https://github.com/jonathandturner/rhai) @@ -61,6 +68,7 @@ mod call; mod engine; mod error; mod fn_register; +mod optimize; mod parser; mod result; mod scope; diff --git a/src/scope.rs b/src/scope.rs index 01bcebda..2c791faa 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -9,13 +9,17 @@ use std::borrow::Cow; /// # Example /// /// ```rust +/// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::{Engine, Scope}; /// /// let mut engine = Engine::new(); /// let mut my_scope = Scope::new(); /// -/// assert!(engine.eval_with_scope::<()>(&mut my_scope, "let x = 5;").is_ok()); -/// assert_eq!(engine.eval_with_scope::(&mut my_scope, "x + 1").unwrap(), 6); +/// engine.eval_with_scope::<()>(&mut my_scope, "let x = 5;")?; +/// +/// assert_eq!(engine.eval_with_scope::(&mut my_scope, "x + 1")?, 6); +/// # Ok(()) +/// # } /// ``` /// /// When searching for variables, newly-added variables are found before similarly-named but older variables, diff --git a/tests/engine.rs b/tests/engine.rs index 8db972c4..b55a7c44 100644 --- a/tests/engine.rs +++ b/tests/engine.rs @@ -4,7 +4,7 @@ use rhai::{Engine, EvalAltResult}; fn test_engine_call_fn() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); - let ast = Engine::compile("fn hello(x, y) { x.len() + y }")?; + let ast = engine.compile("fn hello(x, y) { x.len() + y }")?; let result: i64 = engine.call_fn("hello", &ast, (&mut String::from("abc"), &mut 123_i64))?; diff --git a/tests/var_scope.rs b/tests/var_scope.rs index d1f0c24e..9028022a 100644 --- a/tests/var_scope.rs +++ b/tests/var_scope.rs @@ -40,9 +40,9 @@ fn test_scope_eval() -> Result<(), EvalAltResult> { .expect("y and z not found?"); // Second invocation using the same state - if let Ok(result) = engine.eval_with_scope::(&mut scope, "x") { - println!("result: {}", result); // should print 966 - } + let result = engine.eval_with_scope::(&mut scope, "x")?; + + println!("result: {}", result); // should print 966 // Variable y is changed in the script assert_eq!(scope.get_value::("y").unwrap(), 1); From c467ffd58dfe050c5f36ddec94189f60c8287bf8 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 9 Mar 2020 21:52:43 +0800 Subject: [PATCH 22/27] Comment out optimizer for successful build. --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 56df1ae2..d90fb8cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,7 +68,7 @@ mod call; mod engine; mod error; mod fn_register; -mod optimize; +//mod optimize; mod parser; mod result; mod scope; From 55e7af7b04212b15b80edd59bc15c20eafeda3eb Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 9 Mar 2020 21:57:07 +0800 Subject: [PATCH 23/27] Add AST optimizer. --- src/api.rs | 35 +++++--- src/engine.rs | 104 +++++++++++++----------- src/lib.rs | 2 +- src/optimize.rs | 212 ++++++++++++++++++++++++++++++++++++++++++++++++ src/parser.rs | 84 +++++++++++++------ 5 files changed, 349 insertions(+), 88 deletions(-) create mode 100644 src/optimize.rs diff --git a/src/api.rs b/src/api.rs index 698affa7..34c6d2ef 100644 --- a/src/api.rs +++ b/src/api.rs @@ -95,13 +95,13 @@ impl<'e> Engine<'e> { } /// Compile a string into an AST - pub fn compile(input: &str) -> Result { + pub fn compile(&self, input: &str) -> Result { let tokens = lex(input); - parse(&mut tokens.peekable()) + parse(&mut tokens.peekable(), self.optimize) } /// Compile a file into an AST - pub fn compile_file(filename: &str) -> Result { + pub fn compile_file(&self, filename: &str) -> Result { use std::fs::File; use std::io::prelude::*; @@ -112,7 +112,7 @@ impl<'e> Engine<'e> { f.read_to_string(&mut contents) .map_err(|err| EvalAltResult::ErrorReadingScriptFile(filename.into(), err)) - .and_then(|_| Self::compile(&contents).map_err(EvalAltResult::ErrorParsing)) + .and_then(|_| self.compile(&contents).map_err(EvalAltResult::ErrorParsing)) } /// Evaluate a file @@ -142,7 +142,7 @@ impl<'e> Engine<'e> { scope: &mut Scope, input: &str, ) -> Result { - let ast = Self::compile(input).map_err(EvalAltResult::ErrorParsing)?; + let ast = self.compile(input).map_err(EvalAltResult::ErrorParsing)?; self.eval_ast_with_scope(scope, &ast) } @@ -229,7 +229,7 @@ impl<'e> Engine<'e> { ) -> Result<(), EvalAltResult> { let tokens = lex(input); - parse(&mut tokens.peekable()) + parse(&mut tokens.peekable(), self.optimize) .map_err(|err| EvalAltResult::ErrorParsing(err)) .and_then(|AST(ref statements, ref functions)| { for f in functions { @@ -258,11 +258,12 @@ impl<'e> Engine<'e> { /// # Example /// /// ```rust - /// # use rhai::{Engine, EvalAltResult}; - /// # fn main() -> Result<(), EvalAltResult> { + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::Engine; + /// /// let mut engine = Engine::new(); /// - /// let ast = Engine::compile("fn add(x, y) { x.len() + y }")?; + /// let ast = engine.compile("fn add(x, y) { x.len() + y }")?; /// /// let result: i64 = engine.call_fn("add", &ast, (&mut String::from("abc"), &mut 123_i64))?; /// @@ -309,16 +310,20 @@ impl<'e> Engine<'e> { /// # Example /// /// ```rust - /// # use rhai::Engine; + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::Engine; + /// /// let mut result = String::from(""); /// { /// let mut engine = Engine::new(); /// /// // Override action of 'print' function /// engine.on_print(|s| result.push_str(s)); - /// engine.consume("print(40 + 2);").unwrap(); + /// engine.consume("print(40 + 2);")?; /// } /// assert_eq!(result, "42"); + /// # Ok(()) + /// # } /// ``` pub fn on_print(&mut self, callback: impl FnMut(&str) + 'e) { self.on_print = Box::new(callback); @@ -329,16 +334,20 @@ impl<'e> Engine<'e> { /// # Example /// /// ```rust - /// # use rhai::Engine; + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::Engine; + /// /// let mut result = String::from(""); /// { /// let mut engine = Engine::new(); /// /// // Override action of 'debug' function /// engine.on_debug(|s| result.push_str(s)); - /// engine.consume(r#"debug("hello");"#).unwrap(); + /// engine.consume(r#"debug("hello");"#)?; /// } /// assert_eq!(result, "\"hello\""); + /// # Ok(()) + /// # } /// ``` pub fn on_debug(&mut self, callback: impl FnMut(&str) + 'e) { self.on_debug = Box::new(callback); diff --git a/src/engine.rs b/src/engine.rs index 1c13637e..1177a044 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -4,7 +4,7 @@ use crate::any::{Any, AnyExt, Dynamic, Variant}; use crate::parser::{Expr, FnDef, Position, Stmt}; use crate::result::EvalAltResult; use crate::scope::Scope; -use std::any::TypeId; +use std::any::{type_name, TypeId}; use std::borrow::Cow; use std::cmp::{PartialEq, PartialOrd}; use std::collections::HashMap; @@ -20,11 +20,11 @@ pub type FnAny = dyn Fn(FnCallArgs, Position) -> Result; type IteratorFn = dyn Fn(&Dynamic) -> Box>; -const KEYWORD_PRINT: &'static str = "print"; -const KEYWORD_DEBUG: &'static str = "debug"; -const KEYWORD_TYPE_OF: &'static str = "type_of"; -const FUNC_GETTER: &'static str = "get$"; -const FUNC_SETTER: &'static str = "set$"; +pub(crate) const KEYWORD_PRINT: &'static str = "print"; +pub(crate) const KEYWORD_DEBUG: &'static str = "debug"; +pub(crate) const KEYWORD_TYPE_OF: &'static str = "type_of"; +pub(crate) const FUNC_GETTER: &'static str = "get$"; +pub(crate) const FUNC_SETTER: &'static str = "set$"; #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] enum IndexSourceType { @@ -42,17 +42,20 @@ pub struct FnSpec<'a> { /// Rhai main scripting engine. /// /// ```rust +/// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::Engine; /// -/// fn main() { -/// let mut engine = Engine::new(); +/// let mut engine = Engine::new(); /// -/// if let Ok(result) = engine.eval::("40 + 2") { -/// println!("Answer: {}", result); // prints 42 -/// } -/// } +/// let result = engine.eval::("40 + 2")?; +/// +/// println!("Answer: {}", result); // prints 42 +/// # Ok(()) +/// # } /// ``` pub struct Engine<'e> { + /// Optimize the AST after compilation + pub(crate) optimize: bool, /// A hashmap containing all compiled functions known to the engine pub(crate) ext_functions: HashMap, Arc>>, /// A hashmap containing all script-defined functions @@ -72,6 +75,42 @@ pub enum FnIntExt<'a> { } impl Engine<'_> { + /// Create a new `Engine` + pub fn new() -> Self { + // User-friendly names for built-in types + let type_names = [ + (type_name::(), "string"), + (type_name::(), "array"), + (type_name::(), "dynamic"), + ] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + + // Create the new scripting Engine + let mut engine = Engine { + optimize: true, + ext_functions: HashMap::new(), + script_functions: HashMap::new(), + type_iterators: HashMap::new(), + type_names, + on_print: Box::new(default_print), // default print/debug implementations + on_debug: Box::new(default_print), + }; + + engine.register_core_lib(); + + #[cfg(not(feature = "no_stdlib"))] + engine.register_stdlib(); // Register the standard library when no_stdlib is not set + + engine + } + + /// Control whether the `Engine` will optimize an AST after compilation + pub fn set_optimization(&mut self, optimize: bool) { + self.optimize = optimize + } + /// Universal method for calling functions, that are either /// registered with the `Engine` or written in Rhai pub(crate) fn call_fn_raw( @@ -151,7 +190,7 @@ impl Engine<'_> { ); // Evaluate - match self.eval_stmt(&mut scope, &*func.body) { + match self.eval_stmt(&mut scope, &func.body) { // Convert return statement to return value Err(EvalAltResult::Return(x, _)) => Ok(x), other => other, @@ -726,7 +765,7 @@ impl Engine<'_> { .map(|(_, _, _, x)| x), // Statement block - Expr::Block(block, _) => self.eval_stmt(scope, block), + Expr::Stmt(stmt, _) => self.eval_stmt(scope, stmt), // lhs = rhs Expr::Assignment(lhs, rhs, _) => { @@ -850,11 +889,14 @@ impl Engine<'_> { stmt: &Stmt, ) -> Result { match stmt { + // No-op + Stmt::Noop(_) => Ok(().into_dynamic()), + // Expression as statement Stmt::Expr(expr) => self.eval_expr(scope, expr), // Block scope - Stmt::Block(block) => { + Stmt::Block(block, _) => { let prev_len = scope.len(); let mut last_result: Result = Ok(().into_dynamic()); @@ -988,38 +1030,6 @@ impl Engine<'_> { .map(|s| s.as_str()) .unwrap_or(name) } - - /// Make a new engine - pub fn new<'a>() -> Engine<'a> { - use std::any::type_name; - - // User-friendly names for built-in types - let type_names = [ - (type_name::(), "string"), - (type_name::(), "array"), - (type_name::(), "dynamic"), - ] - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - - // Create the new scripting Engine - let mut engine = Engine { - ext_functions: HashMap::new(), - script_functions: HashMap::new(), - type_iterators: HashMap::new(), - type_names, - on_print: Box::new(default_print), // default print/debug implementations - on_debug: Box::new(default_print), - }; - - engine.register_core_lib(); - - #[cfg(not(feature = "no_stdlib"))] - engine.register_stdlib(); // Register the standard library when no_stdlib is not set - - engine - } } /// Print/debug to stdout diff --git a/src/lib.rs b/src/lib.rs index d90fb8cb..56df1ae2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,7 +68,7 @@ mod call; mod engine; mod error; mod fn_register; -//mod optimize; +mod optimize; mod parser; mod result; mod scope; diff --git a/src/optimize.rs b/src/optimize.rs new file mode 100644 index 00000000..a9445e5d --- /dev/null +++ b/src/optimize.rs @@ -0,0 +1,212 @@ +use crate::parser::{Expr, Stmt}; + +fn optimize_stmt(stmt: Stmt, changed: &mut bool) -> Stmt { + match stmt { + Stmt::IfElse(expr, stmt1, None) => match *expr { + Expr::False(pos) => { + *changed = true; + Stmt::Noop(pos) + } + Expr::True(_) => optimize_stmt(*stmt1, changed), + _ => Stmt::IfElse( + Box::new(optimize_expr(*expr, changed)), + Box::new(optimize_stmt(*stmt1, changed)), + None, + ), + }, + + Stmt::IfElse(expr, stmt1, Some(stmt2)) => match *expr { + Expr::False(_) => optimize_stmt(*stmt2, changed), + Expr::True(_) => optimize_stmt(*stmt1, changed), + _ => Stmt::IfElse( + Box::new(optimize_expr(*expr, changed)), + Box::new(optimize_stmt(*stmt1, changed)), + Some(Box::new(optimize_stmt(*stmt2, changed))), + ), + }, + + Stmt::While(expr, stmt) => match *expr { + Expr::False(pos) => { + *changed = true; + Stmt::Noop(pos) + } + Expr::True(_) => Stmt::Loop(Box::new(optimize_stmt(*stmt, changed))), + _ => Stmt::While( + Box::new(optimize_expr(*expr, changed)), + Box::new(optimize_stmt(*stmt, changed)), + ), + }, + + Stmt::Loop(stmt) => Stmt::Loop(Box::new(optimize_stmt(*stmt, changed))), + Stmt::For(id, expr, stmt) => Stmt::For( + id, + Box::new(optimize_expr(*expr, changed)), + Box::new(optimize_stmt(*stmt, changed)), + ), + Stmt::Let(id, Some(expr), pos) => { + Stmt::Let(id, Some(Box::new(optimize_expr(*expr, changed))), pos) + } + Stmt::Let(_, None, _) => stmt, + + Stmt::Block(statements, pos) => { + let original_len = statements.len(); + + let mut result: Vec<_> = statements + .into_iter() // For each statement + .map(|s| optimize_stmt(s, changed)) // Optimize the statement + .filter(Stmt::is_op) // Remove no-op's + .collect(); + + *changed = *changed || original_len != result.len(); + + match result[..] { + [] => { + // No statements in block - change to No-op + *changed = true; + Stmt::Noop(pos) + } + [Stmt::Let(_, _, _)] => { + // Only one let statement, but cannot promote + // (otherwise the variable gets declared in the scope above) + // and still need to run just in case there are side effects + Stmt::Block(result, pos) + } + [_] => { + // No statements in block - change to No-op + *changed = true; + result.remove(0) + } + _ => Stmt::Block(result, pos), + } + } + + Stmt::Expr(expr) => Stmt::Expr(Box::new(optimize_expr(*expr, changed))), + + Stmt::ReturnWithVal(Some(expr), is_return, pos) => Stmt::ReturnWithVal( + Some(Box::new(optimize_expr(*expr, changed))), + is_return, + pos, + ), + Stmt::ReturnWithVal(None, _, _) => stmt, + + Stmt::Noop(_) | Stmt::Break(_) => stmt, + } +} + +fn optimize_expr(expr: Expr, changed: &mut bool) -> Expr { + match expr { + Expr::IntegerConstant(_, _) + | Expr::FloatConstant(_, _) + | Expr::Identifier(_, _) + | Expr::CharConstant(_, _) + | Expr::StringConstant(_, _) + | Expr::True(_) + | Expr::False(_) + | Expr::Unit(_) => expr, + + Expr::Stmt(stmt, pos) => match optimize_stmt(*stmt, changed) { + Stmt::Noop(_) => { + *changed = true; + Expr::Unit(pos) + } + Stmt::Expr(expr) => { + *changed = true; + *expr + } + stmt => Expr::Stmt(Box::new(stmt), pos), + }, + Expr::Assignment(id, expr, pos) => { + Expr::Assignment(id, Box::new(optimize_expr(*expr, changed)), pos) + } + Expr::Dot(lhs, rhs, pos) => Expr::Dot( + Box::new(optimize_expr(*lhs, changed)), + Box::new(optimize_expr(*rhs, changed)), + pos, + ), + Expr::Index(lhs, rhs, pos) => Expr::Index( + Box::new(optimize_expr(*lhs, changed)), + Box::new(optimize_expr(*rhs, changed)), + pos, + ), + Expr::Array(items, pos) => { + let original_len = items.len(); + + let items: Vec<_> = items + .into_iter() + .map(|expr| optimize_expr(expr, changed)) + .collect(); + + *changed = *changed || original_len != items.len(); + + Expr::Array(items, pos) + } + + Expr::And(lhs, rhs) => match (*lhs, *rhs) { + (Expr::True(_), rhs) => { + *changed = true; + rhs + } + (Expr::False(pos), _) => { + *changed = true; + Expr::False(pos) + } + (lhs, Expr::True(_)) => { + *changed = true; + lhs + } + (lhs, rhs) => Expr::And( + Box::new(optimize_expr(lhs, changed)), + Box::new(optimize_expr(rhs, changed)), + ), + }, + Expr::Or(lhs, rhs) => match (*lhs, *rhs) { + (Expr::False(_), rhs) => { + *changed = true; + rhs + } + (Expr::True(pos), _) => { + *changed = true; + Expr::True(pos) + } + (lhs, Expr::False(_)) => { + *changed = true; + lhs + } + (lhs, rhs) => Expr::Or( + Box::new(optimize_expr(lhs, changed)), + Box::new(optimize_expr(rhs, changed)), + ), + }, + + Expr::FunctionCall(id, args, def_value, pos) => { + let original_len = args.len(); + + let args: Vec<_> = args + .into_iter() + .map(|a| optimize_expr(a, changed)) + .collect(); + + *changed = *changed || original_len != args.len(); + + Expr::FunctionCall(id, args, def_value, pos) + } + } +} + +pub(crate) fn optimize(mut statements: Vec) -> Vec { + loop { + let mut changed = false; + + statements = statements + .into_iter() + .map(|stmt| optimize_stmt(stmt, &mut changed)) + .filter(Stmt::is_op) + .collect(); + + if !changed { + break; + } + } + + statements +} diff --git a/src/parser.rs b/src/parser.rs index 84a67007..dd5a6ad2 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,7 +2,8 @@ use crate::any::Dynamic; use crate::error::{LexError, ParseError, ParseErrorType}; -use std::{borrow::Cow, char, fmt, iter::Peekable, str::Chars, usize}; +use crate::optimize::optimize; +use std::{borrow::Cow, char, fmt, iter::Peekable, str::Chars, str::FromStr, usize}; type LERR = LexError; type PERR = ParseErrorType; @@ -128,23 +129,33 @@ pub struct AST(pub(crate) Vec, pub(crate) Vec>); pub struct FnDef<'a> { pub name: Cow<'a, str>, pub params: Vec>, - pub body: Box, + pub body: Stmt, pub pos: Position, } #[derive(Debug, Clone)] pub enum Stmt { + Noop(Position), IfElse(Box, Box, Option>), While(Box, Box), Loop(Box), For(String, Box, Box), Let(String, Option>, Position), - Block(Vec), + Block(Vec, Position), Expr(Box), Break(Position), ReturnWithVal(Option>, bool, Position), } +impl Stmt { + pub fn is_op(&self) -> bool { + match self { + Stmt::Noop(_) => false, + _ => true, + } + } +} + #[derive(Debug, Clone)] pub enum Expr { IntegerConstant(i64, Position), @@ -152,7 +163,7 @@ pub enum Expr { Identifier(String, Position), CharConstant(char, Position), StringConstant(String, Position), - Block(Box, Position), + Stmt(Box, Position), FunctionCall(String, Vec, Option, Position), Assignment(Box, Box, Position), Dot(Box, Box, Position), @@ -174,7 +185,7 @@ impl Expr { | Expr::CharConstant(_, pos) | Expr::StringConstant(_, pos) | Expr::FunctionCall(_, _, _, pos) - | Expr::Block(_, pos) + | Expr::Stmt(_, pos) | Expr::Array(_, pos) | Expr::True(pos) | Expr::False(pos) @@ -666,24 +677,23 @@ impl<'a> TokenIterator<'a> { let out: String = result.iter().skip(2).filter(|&&c| c != '_').collect(); return Some(( - if let Ok(val) = i64::from_str_radix(&out, radix) { - Token::IntegerConstant(val) - } else { - Token::LexError(LERR::MalformedNumber(result.iter().collect())) - }, + i64::from_str_radix(&out, radix) + .map(Token::IntegerConstant) + .unwrap_or_else(|_| { + Token::LexError(LERR::MalformedNumber(result.iter().collect())) + }), pos, )); } else { let out: String = result.iter().filter(|&&c| c != '_').collect(); return Some(( - if let Ok(val) = out.parse::() { - Token::IntegerConstant(val) - } else if let Ok(val) = out.parse::() { - Token::FloatConstant(val) - } else { - Token::LexError(LERR::MalformedNumber(result.iter().collect())) - }, + i64::from_str(&out) + .map(Token::IntegerConstant) + .or_else(|_| f64::from_str(&out).map(Token::FloatConstant)) + .unwrap_or_else(|_| { + Token::LexError(LERR::MalformedNumber(result.iter().collect())) + }), pos, )); } @@ -1288,7 +1298,7 @@ fn parse_primary<'a>(input: &mut Peekable>) -> Result { - return parse_block(input).map(|block| Expr::Block(Box::new(block), pos)) + return parse_block(input).map(|block| Expr::Stmt(Box::new(block), pos)) } _ => (), } @@ -1667,7 +1677,7 @@ fn parse_block<'a>(input: &mut Peekable>) -> Result return Err(ParseError::new(PERR::MissingLeftBrace, Position::eof())), } - input.next(); + let pos = input.next().unwrap().1; let mut statements = Vec::new(); @@ -1695,7 +1705,7 @@ fn parse_block<'a>(input: &mut Peekable>) -> Result { input.next(); - Ok(Stmt::Block(statements)) + Ok(Stmt::Block(statements, pos)) } Some(&(_, pos)) => Err(ParseError::new( PERR::MissingRightBrace("end of block".into()), @@ -1811,13 +1821,16 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { +fn parse_top_level<'a>( + input: &mut Peekable>, + optimize_ast: bool, +) -> Result { let mut statements = Vec::new(); let mut functions = Vec::new(); @@ -1833,9 +1846,26 @@ fn parse_top_level<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { - parse_top_level(input) +pub fn parse<'a>( + input: &mut Peekable>, + optimize_ast: bool, +) -> Result { + parse_top_level(input, optimize_ast) } From bae99462914e9168fd7706e229231c2b82be292a Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 10 Mar 2020 09:30:12 +0800 Subject: [PATCH 24/27] Allow engine to retain functions across runs. --- examples/reuse_scope.rs | 4 +-- src/api.rs | 55 +++++++++++++++++++++------------- src/engine.rs | 5 ++++ src/error.rs | 2 +- src/result.rs | 65 ++++++++++++++++++++++++++++------------- src/scope.rs | 6 ++-- tests/var_scope.rs | 25 +++++++--------- 7 files changed, 101 insertions(+), 61 deletions(-) diff --git a/examples/reuse_scope.rs b/examples/reuse_scope.rs index a6509ffe..c83d0a72 100644 --- a/examples/reuse_scope.rs +++ b/examples/reuse_scope.rs @@ -4,9 +4,9 @@ fn main() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); let mut scope = Scope::new(); - engine.eval_with_scope::<()>(&mut scope, "let x = 4 + 5")?; + engine.eval_with_scope::<()>(&mut scope, false, "let x = 4 + 5")?; - let result = engine.eval_with_scope::(&mut scope, "x")?; + let result = engine.eval_with_scope::(&mut scope, false, "x")?; println!("result: {}", result); diff --git a/src/api.rs b/src/api.rs index 34c6d2ef..ca6fe88f 100644 --- a/src/api.rs +++ b/src/api.rs @@ -94,13 +94,13 @@ impl<'e> Engine<'e> { self.register_set(name, set_fn); } - /// Compile a string into an AST + /// Compile a string into an AST. pub fn compile(&self, input: &str) -> Result { let tokens = lex(input); parse(&mut tokens.peekable(), self.optimize) } - /// Compile a file into an AST + /// Compile a file into an AST. pub fn compile_file(&self, filename: &str) -> Result { use std::fs::File; use std::io::prelude::*; @@ -115,7 +115,7 @@ impl<'e> Engine<'e> { .and_then(|_| self.compile(&contents).map_err(EvalAltResult::ErrorParsing)) } - /// Evaluate a file + /// Evaluate a file. pub fn eval_file(&mut self, filename: &str) -> Result { use std::fs::File; use std::io::prelude::*; @@ -130,32 +130,40 @@ impl<'e> Engine<'e> { .and_then(|_| self.eval::(&contents)) } - /// Evaluate a string + /// Evaluate a string. pub fn eval(&mut self, input: &str) -> Result { let mut scope = Scope::new(); - self.eval_with_scope(&mut scope, input) + self.eval_with_scope(&mut scope, false, input) } - /// Evaluate a string with own scope + /// Evaluate a string with own scope. + /// + /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ + /// and not cleared from run to run. pub fn eval_with_scope( &mut self, scope: &mut Scope, + retain_functions: bool, input: &str, ) -> Result { let ast = self.compile(input).map_err(EvalAltResult::ErrorParsing)?; - self.eval_ast_with_scope(scope, &ast) + self.eval_ast_with_scope(scope, retain_functions, &ast) } - /// Evaluate an AST + /// Evaluate an AST. pub fn eval_ast(&mut self, ast: &AST) -> Result { let mut scope = Scope::new(); - self.eval_ast_with_scope(&mut scope, ast) + self.eval_ast_with_scope(&mut scope, false, ast) } - /// Evaluate an AST with own scope + /// Evaluate an AST with own scope. + /// + /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ + /// and not cleared from run to run. pub fn eval_ast_with_scope( &mut self, scope: &mut Scope, + retain_functions: bool, ast: &AST, ) -> Result { let AST(statements, functions) = ast; @@ -174,7 +182,9 @@ impl<'e> Engine<'e> { .iter() .try_fold(().into_dynamic(), |_, stmt| self.eval_stmt(scope, stmt)); - self.script_functions.clear(); // Clean up engine + if !retain_functions { + self.clear_functions(); + } match result { Err(EvalAltResult::Return(out, pos)) => out.downcast::().map(|v| *v).map_err(|a| { @@ -196,8 +206,7 @@ impl<'e> Engine<'e> { } /// Evaluate a file, but throw away the result and only return error (if any). - /// Useful for when you don't need the result, but still need - /// to keep track of possible errors + /// Useful for when you don't need the result, but still need to keep track of possible errors. pub fn consume_file(&mut self, filename: &str) -> Result<(), EvalAltResult> { use std::fs::File; use std::io::prelude::*; @@ -213,18 +222,20 @@ impl<'e> Engine<'e> { } /// Evaluate a string, but throw away the result and only return error (if any). - /// Useful for when you don't need the result, but still need - /// to keep track of possible errors + /// Useful for when you don't need the result, but still need to keep track of possible errors. pub fn consume(&mut self, input: &str) -> Result<(), EvalAltResult> { - self.consume_with_scope(&mut Scope::new(), input) + self.consume_with_scope(&mut Scope::new(), false, input) } - /// Evaluate a string with own scope, but throw away the result and only return error (if any). - /// Useful for when you don't need the result, but still need - /// to keep track of possible errors + /// Evaluate a string, but throw away the result and only return error (if any). + /// Useful for when you don't need the result, but still need to keep track of possible errors. + /// + /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ + /// and not cleared from run to run. pub fn consume_with_scope( &mut self, scope: &mut Scope, + retain_functions: bool, input: &str, ) -> Result<(), EvalAltResult> { let tokens = lex(input); @@ -247,7 +258,9 @@ impl<'e> Engine<'e> { .try_fold(().into_dynamic(), |_, o| self.eval_stmt(scope, o)) .map(|_| ()); - self.script_functions.clear(); // Clean up engine + if !retain_functions { + self.clear_functions(); + } val }) @@ -300,7 +313,7 @@ impl<'e> Engine<'e> { }) }); - self.script_functions.clear(); // Clean up engine + self.clear_functions(); result } diff --git a/src/engine.rs b/src/engine.rs index 1177a044..5806abb2 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1030,6 +1030,11 @@ impl Engine<'_> { .map(|s| s.as_str()) .unwrap_or(name) } + + /// Clean up all script-defined functions within the `Engine`. + pub fn clear_functions(&mut self) { + self.script_functions.clear(); + } } /// Print/debug to stdout diff --git a/src/error.rs b/src/error.rs index 5c4419d4..8733682b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -81,7 +81,7 @@ pub enum ParseErrorType { /// Error when parsing a script. #[derive(Debug, PartialEq, Clone)] -pub struct ParseError(ParseErrorType, Position); +pub struct ParseError(pub(crate) ParseErrorType, pub(crate) Position); impl ParseError { /// Create a new `ParseError`. diff --git a/src/result.rs b/src/result.rs index 95b5ee12..224c8232 100644 --- a/src/result.rs +++ b/src/result.rs @@ -174,29 +174,54 @@ impl From for EvalAltResult { } impl EvalAltResult { + pub fn position(&self) -> Position { + match self { + Self::ErrorReadingScriptFile(_, _) | Self::LoopBreak => Position::none(), + + Self::ErrorParsing(err) => err.position(), + + Self::ErrorFunctionNotFound(_, pos) + | Self::ErrorFunctionArgsMismatch(_, _, _, pos) + | Self::ErrorBooleanArgMismatch(_, pos) + | Self::ErrorCharMismatch(pos) + | Self::ErrorArrayBounds(_, _, pos) + | Self::ErrorStringBounds(_, _, pos) + | Self::ErrorIndexingType(_, pos) + | Self::ErrorIndexExpr(pos) + | Self::ErrorIfGuard(pos) + | Self::ErrorFor(pos) + | Self::ErrorVariableNotFound(_, pos) + | Self::ErrorAssignmentToUnknownLHS(pos) + | Self::ErrorMismatchOutputType(_, pos) + | Self::ErrorDotExpr(_, pos) + | Self::ErrorArithmetic(_, pos) + | Self::ErrorRuntime(_, pos) + | Self::Return(_, pos) => *pos, + } + } + pub(crate) fn set_position(&mut self, new_position: Position) { match self { - EvalAltResult::ErrorReadingScriptFile(_, _) - | EvalAltResult::LoopBreak - | EvalAltResult::ErrorParsing(_) => (), + Self::ErrorReadingScriptFile(_, _) | Self::LoopBreak => (), - EvalAltResult::ErrorFunctionNotFound(_, ref mut pos) - | EvalAltResult::ErrorFunctionArgsMismatch(_, _, _, ref mut pos) - | EvalAltResult::ErrorBooleanArgMismatch(_, ref mut pos) - | EvalAltResult::ErrorCharMismatch(ref mut pos) - | EvalAltResult::ErrorArrayBounds(_, _, ref mut pos) - | EvalAltResult::ErrorStringBounds(_, _, ref mut pos) - | EvalAltResult::ErrorIndexingType(_, ref mut pos) - | EvalAltResult::ErrorIndexExpr(ref mut pos) - | EvalAltResult::ErrorIfGuard(ref mut pos) - | EvalAltResult::ErrorFor(ref mut pos) - | EvalAltResult::ErrorVariableNotFound(_, ref mut pos) - | EvalAltResult::ErrorAssignmentToUnknownLHS(ref mut pos) - | EvalAltResult::ErrorMismatchOutputType(_, ref mut pos) - | EvalAltResult::ErrorDotExpr(_, ref mut pos) - | EvalAltResult::ErrorArithmetic(_, ref mut pos) - | EvalAltResult::ErrorRuntime(_, ref mut pos) - | EvalAltResult::Return(_, ref mut pos) => *pos = new_position, + Self::ErrorParsing(ParseError(_, ref mut pos)) + | Self::ErrorFunctionNotFound(_, ref mut pos) + | Self::ErrorFunctionArgsMismatch(_, _, _, ref mut pos) + | Self::ErrorBooleanArgMismatch(_, ref mut pos) + | Self::ErrorCharMismatch(ref mut pos) + | Self::ErrorArrayBounds(_, _, ref mut pos) + | Self::ErrorStringBounds(_, _, ref mut pos) + | Self::ErrorIndexingType(_, ref mut pos) + | Self::ErrorIndexExpr(ref mut pos) + | Self::ErrorIfGuard(ref mut pos) + | Self::ErrorFor(ref mut pos) + | Self::ErrorVariableNotFound(_, ref mut pos) + | Self::ErrorAssignmentToUnknownLHS(ref mut pos) + | Self::ErrorMismatchOutputType(_, ref mut pos) + | Self::ErrorDotExpr(_, ref mut pos) + | Self::ErrorArithmetic(_, ref mut pos) + | Self::ErrorRuntime(_, ref mut pos) + | Self::Return(_, ref mut pos) => *pos = new_position, } } } diff --git a/src/scope.rs b/src/scope.rs index 2c791faa..2e2c492b 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -4,7 +4,7 @@ use crate::any::{Any, Dynamic}; use std::borrow::Cow; /// A type containing information about current scope. -/// Useful for keeping state between `Engine` runs +/// Useful for keeping state between `Engine` runs. /// /// # Example /// @@ -15,9 +15,9 @@ use std::borrow::Cow; /// let mut engine = Engine::new(); /// let mut my_scope = Scope::new(); /// -/// engine.eval_with_scope::<()>(&mut my_scope, "let x = 5;")?; +/// engine.eval_with_scope::<()>(&mut my_scope, false, "let x = 5;")?; /// -/// assert_eq!(engine.eval_with_scope::(&mut my_scope, "x + 1")?, 6); +/// assert_eq!(engine.eval_with_scope::(&mut my_scope, false, "x + 1")?, 6); /// # Ok(()) /// # } /// ``` diff --git a/tests/var_scope.rs b/tests/var_scope.rs index 9028022a..f8371bb3 100644 --- a/tests/var_scope.rs +++ b/tests/var_scope.rs @@ -5,12 +5,15 @@ fn test_var_scope() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); let mut scope = Scope::new(); - engine.eval_with_scope::<()>(&mut scope, "let x = 4 + 5")?; - assert_eq!(engine.eval_with_scope::(&mut scope, "x")?, 9); - engine.eval_with_scope::<()>(&mut scope, "x = x + 1; x = x + 2;")?; - assert_eq!(engine.eval_with_scope::(&mut scope, "x")?, 12); - assert_eq!(engine.eval_with_scope::<()>(&mut scope, "{let x = 3}")?, ()); - assert_eq!(engine.eval_with_scope::(&mut scope, "x")?, 12); + engine.eval_with_scope::<()>(&mut scope, false, "let x = 4 + 5")?; + assert_eq!(engine.eval_with_scope::(&mut scope, false, "x")?, 9); + engine.eval_with_scope::<()>(&mut scope, false, "x = x + 1; x = x + 2;")?; + assert_eq!(engine.eval_with_scope::(&mut scope, false, "x")?, 12); + assert_eq!( + engine.eval_with_scope::<()>(&mut scope, false, "{let x = 3}")?, + () + ); + assert_eq!(engine.eval_with_scope::(&mut scope, false, "x")?, 12); Ok(()) } @@ -30,17 +33,11 @@ fn test_scope_eval() -> Result<(), EvalAltResult> { // First invocation engine - .eval_with_scope::<()>( - &mut scope, - r" - let x = 4 + 5 - y + z; - y = 1; - ", - ) + .eval_with_scope::<()>(&mut scope, false, " let x = 4 + 5 - y + z; y = 1;") .expect("y and z not found?"); // Second invocation using the same state - let result = engine.eval_with_scope::(&mut scope, "x")?; + let result = engine.eval_with_scope::(&mut scope, false, "x")?; println!("result: {}", result); // should print 966 From feaad4e0da15a54d540abfcb33c094031553460d Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 10 Mar 2020 10:07:44 +0800 Subject: [PATCH 25/27] Group use of std under one root. --- src/any.rs | 6 ++++-- src/api.rs | 17 ++++++----------- src/builtin.rs | 9 ++++++--- src/engine.rs | 14 ++++++++------ 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/any.rs b/src/any.rs index c2003da4..96fe099b 100644 --- a/src/any.rs +++ b/src/any.rs @@ -1,7 +1,9 @@ //! Helper module which defines the `Any` trait to to allow dynamic value handling. -use std::any::{type_name, TypeId}; -use std::fmt; +use std::{ + any::{type_name, TypeId}, + fmt, +}; /// An raw value of any type. pub type Variant = dyn Any; diff --git a/src/api.rs b/src/api.rs index ca6fe88f..5e89c5ca 100644 --- a/src/api.rs +++ b/src/api.rs @@ -8,8 +8,12 @@ use crate::fn_register::RegisterFn; use crate::parser::{lex, parse, Position, AST}; use crate::result::EvalAltResult; use crate::scope::Scope; -use std::any::{type_name, TypeId}; -use std::sync::Arc; +use std::{ + any::{type_name, TypeId}, + fs::File, + io::prelude::*, + sync::Arc, +}; impl<'e> Engine<'e> { pub(crate) fn register_fn_raw( @@ -102,9 +106,6 @@ impl<'e> Engine<'e> { /// Compile a file into an AST. pub fn compile_file(&self, filename: &str) -> Result { - use std::fs::File; - use std::io::prelude::*; - let mut f = File::open(filename) .map_err(|err| EvalAltResult::ErrorReadingScriptFile(filename.into(), err))?; @@ -117,9 +118,6 @@ impl<'e> Engine<'e> { /// Evaluate a file. pub fn eval_file(&mut self, filename: &str) -> Result { - use std::fs::File; - use std::io::prelude::*; - let mut f = File::open(filename) .map_err(|err| EvalAltResult::ErrorReadingScriptFile(filename.into(), err))?; @@ -208,9 +206,6 @@ impl<'e> Engine<'e> { /// Evaluate a file, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. pub fn consume_file(&mut self, filename: &str) -> Result<(), EvalAltResult> { - use std::fs::File; - use std::io::prelude::*; - let mut f = File::open(filename) .map_err(|err| EvalAltResult::ErrorReadingScriptFile(filename.into(), err))?; diff --git a/src/builtin.rs b/src/builtin.rs index 546d680f..6cce507e 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -4,9 +4,12 @@ use crate::any::Any; use crate::engine::{Array, Engine}; use crate::fn_register::RegisterFn; -use std::fmt::{Debug, Display}; -use std::ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Neg, Range, Rem, Sub}; -use std::{i32, i64, u32}; +use std::{ + fmt::{Debug, Display}, + i32, i64, + ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Neg, Range, Rem, Sub}, + u32, +}; #[cfg(feature = "unchecked")] use std::ops::{Shl, Shr}; diff --git a/src/engine.rs b/src/engine.rs index 5806abb2..89dac01c 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -4,12 +4,14 @@ use crate::any::{Any, AnyExt, Dynamic, Variant}; use crate::parser::{Expr, FnDef, Position, Stmt}; use crate::result::EvalAltResult; use crate::scope::Scope; -use std::any::{type_name, TypeId}; -use std::borrow::Cow; -use std::cmp::{PartialEq, PartialOrd}; -use std::collections::HashMap; -use std::iter::once; -use std::sync::Arc; +use std::{ + any::{type_name, TypeId}, + borrow::Cow, + cmp::{PartialEq, PartialOrd}, + collections::HashMap, + iter::once, + sync::Arc, +}; /// An dynamic array of `Dynamic` values. pub type Array = Vec; From 2d80ee2f1874d733d328cf21fe226dcb462ba001 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 10 Mar 2020 11:22:41 +0800 Subject: [PATCH 26/27] Refine optimizer. --- src/optimize.rs | 36 ++++++++++++++++++++++++++---------- src/parser.rs | 15 +++++++++++++++ 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/optimize.rs b/src/optimize.rs index a9445e5d..4a0b26b1 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -8,8 +8,8 @@ fn optimize_stmt(stmt: Stmt, changed: &mut bool) -> Stmt { Stmt::Noop(pos) } Expr::True(_) => optimize_stmt(*stmt1, changed), - _ => Stmt::IfElse( - Box::new(optimize_expr(*expr, changed)), + expr => Stmt::IfElse( + Box::new(optimize_expr(expr, changed)), Box::new(optimize_stmt(*stmt1, changed)), None, ), @@ -18,8 +18,8 @@ fn optimize_stmt(stmt: Stmt, changed: &mut bool) -> Stmt { Stmt::IfElse(expr, stmt1, Some(stmt2)) => match *expr { Expr::False(_) => optimize_stmt(*stmt2, changed), Expr::True(_) => optimize_stmt(*stmt1, changed), - _ => Stmt::IfElse( - Box::new(optimize_expr(*expr, changed)), + expr => Stmt::IfElse( + Box::new(optimize_expr(expr, changed)), Box::new(optimize_stmt(*stmt1, changed)), Some(Box::new(optimize_stmt(*stmt2, changed))), ), @@ -31,8 +31,8 @@ fn optimize_stmt(stmt: Stmt, changed: &mut bool) -> Stmt { Stmt::Noop(pos) } Expr::True(_) => Stmt::Loop(Box::new(optimize_stmt(*stmt, changed))), - _ => Stmt::While( - Box::new(optimize_expr(*expr, changed)), + expr => Stmt::While( + Box::new(optimize_expr(expr, changed)), Box::new(optimize_stmt(*stmt, changed)), ), }, @@ -57,6 +57,17 @@ fn optimize_stmt(stmt: Stmt, changed: &mut bool) -> Stmt { .filter(Stmt::is_op) // Remove no-op's .collect(); + if let Some(last_stmt) = result.pop() { + // Remove all raw expression statements that evaluate to constants + // except for the very last statement + result.retain(|stmt| match stmt { + Stmt::Expr(expr) if expr.is_constant() => false, + _ => true, + }); + + result.push(last_stmt); + } + *changed = *changed || original_len != result.len(); match result[..] { @@ -65,14 +76,19 @@ fn optimize_stmt(stmt: Stmt, changed: &mut bool) -> Stmt { *changed = true; Stmt::Noop(pos) } - [Stmt::Let(_, _, _)] => { + [Stmt::Let(_, None, _)] => { + // Only one empty variable declaration - change to No-op + *changed = true; + Stmt::Noop(pos) + } + [Stmt::Let(_, Some(_), _)] => { // Only one let statement, but cannot promote // (otherwise the variable gets declared in the scope above) // and still need to run just in case there are side effects Stmt::Block(result, pos) } [_] => { - // No statements in block - change to No-op + // Only one statement - promote *changed = true; result.remove(0) } @@ -87,9 +103,9 @@ fn optimize_stmt(stmt: Stmt, changed: &mut bool) -> Stmt { is_return, pos, ), - Stmt::ReturnWithVal(None, _, _) => stmt, + stmt @ Stmt::ReturnWithVal(None, _, _) => stmt, - Stmt::Noop(_) | Stmt::Break(_) => stmt, + stmt @ Stmt::Noop(_) | stmt @ Stmt::Break(_) => stmt, } } diff --git a/src/parser.rs b/src/parser.rs index dd5a6ad2..10e629d0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -198,6 +198,21 @@ impl Expr { | Expr::Or(e, _) => e.position(), } } + + pub fn is_constant(&self) -> bool { + match self { + Expr::IntegerConstant(_, _) + | Expr::FloatConstant(_, _) + | Expr::Identifier(_, _) + | Expr::CharConstant(_, _) + | Expr::StringConstant(_, _) + | Expr::True(_) + | Expr::False(_) + | Expr::Unit(_) => true, + + _ => false, + } + } } #[derive(Debug, PartialEq, Clone)] From 711cd9bb1c89f79234b14f18509876c67956a918 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 10 Mar 2020 11:25:34 +0800 Subject: [PATCH 27/27] Improve repl and rhai_runner examples with error messages. --- README.md | 40 +++++++++++------- examples/repl.rs | 75 ++++++++++++++++++++++++++-------- examples/rhai_runner.rs | 90 ++++++++++++++++++++++++++++++++--------- 3 files changed, 154 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index a135bcae..71f33028 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Other cool projects to check out: Examples -------- -The repository contains several examples in the `examples` folder: +A number of examples can be found in the `examples` folder: | Example | Description | | -------------------------- | ------------------------------------------------------------------------- | @@ -65,7 +65,7 @@ The repository contains several examples in the `examples` folder: | `reuse_scope` | evaluates two pieces of code in separate runs, but using a common scope | | `rhai_runner` | runs each filename passed to it as a Rhai script | | `simple_fn` | shows how to register a Rust function to a Rhai engine | -| `repl` | a simple REPL, see source code for what it can do at the moment | +| `repl` | a simple REPL, interactively evaluate statements from stdin | Examples can be run with the following command: @@ -73,10 +73,13 @@ Examples can be run with the following command: cargo run --example name ``` +The `repl` example is a particularly good one as it allows you to interactively try out Rhai's +language features in a standard REPL (**R**ead-**E**val-**P**rint **L**oop). + Example Scripts --------------- -We also have a few examples scripts that showcase Rhai's features, all stored in the `scripts` folder: +There are also a number of examples scripts that showcase Rhai's features, all in the `scripts` folder: | Script | Description | | --------------------- | ------------------------------------------------------------- | @@ -96,8 +99,7 @@ We also have a few examples scripts that showcase Rhai's features, all stored in | `string.rhai` | string operations | | `while.rhai` | while loop | -To run the scripts, you can either make your own tiny program, or make use of the `rhai_runner` -example program: +To run the scripts, either make a tiny program or use of the `rhai_runner` example: ```bash cargo run --example rhai_runner scripts/any_script.rhai @@ -106,12 +108,13 @@ cargo run --example rhai_runner scripts/any_script.rhai Hello world ----------- -To get going with Rhai, you create an instance of the scripting engine and then run eval. +To get going with Rhai, create an instance of the scripting engine and then call `eval`: ```rust use rhai::{Engine, EvalAltResult}; -fn main() -> Result<(), EvalAltResult> { +fn main() -> Result<(), EvalAltResult> +{ let mut engine = Engine::new(); let result = engine.eval::("40 + 2")?; @@ -232,7 +235,8 @@ fn get_an_any() -> Dynamic { Box::new(42_i64) } -fn main() -> Result<(), EvalAltResult> { +fn main() -> Result<(), EvalAltResult> +{ let mut engine = Engine::new(); engine.register_fn("add", add); @@ -278,7 +282,8 @@ fn showit(x: &mut T) -> () { println!("{}", x) } -fn main() { +fn main() +{ let mut engine = Engine::new(); engine.register_fn("print", showit as fn(x: &mut i64)->()); @@ -310,7 +315,8 @@ fn safe_divide(x: i64, y: i64) -> Result { } } -fn main() { +fn main() +{ let mut engine = Engine::new(); // Fallible functions that return Result values must use register_result_fn() @@ -360,7 +366,8 @@ impl TestStruct { } } -fn main() -> Result<(), EvalAltResult> { +fn main() -> Result<(), EvalAltResult> +{ let mut engine = Engine::new(); engine.register_type::(); @@ -492,7 +499,8 @@ In this example, we first create a state with a few initialized variables, then ```rust use rhai::{Engine, Scope, EvalAltResult}; -fn main() -> Result<(), EvalAltResult> { +fn main() -> Result<(), EvalAltResult> +{ let mut engine = Engine::new(); // First create the state @@ -505,13 +513,15 @@ fn main() -> Result<(), EvalAltResult> { scope.push("z".into(), 999_i64); // First invocation - engine.eval_with_scope::<()>(&mut scope, r" + // (the second boolean argument indicates that we don't need to retain function definitions + // because we didn't declare any!) + engine.eval_with_scope::<()>(&mut scope, false, r" let x = 4 + 5 - y + z; y = 1; ")?; // Second invocation using the same state - let result = engine.eval_with_scope::(&mut scope, "x")?; + let result = engine.eval_with_scope::(&mut scope, false, "x")?; println!("result: {}", result); // should print 966 @@ -545,7 +555,7 @@ let /* intruder comment */ name = "Bob"; Variables --------- -Variables in `Rhai` follow normal naming rules (i.e. must contain only ASCII letters, digits and '`_`' underscores). +Variables in Rhai follow normal naming rules (i.e. must contain only ASCII letters, digits and '`_`' underscores). ```rust let x = 3; diff --git a/examples/repl.rs b/examples/repl.rs index 1990afed..a4221614 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -1,25 +1,66 @@ -use rhai::{Engine, RegisterFn, Scope}; -use std::io::{stdin, stdout, Write}; -use std::process::exit; +use rhai::{Engine, EvalAltResult, Scope}; +use std::{ + io::{stdin, stdout, Write}, + iter, +}; -pub fn main() { - let mut engine = Engine::new(); - let mut scope = Scope::new(); +fn print_error(input: &str, err: EvalAltResult) { + fn padding(pad: &str, len: usize) -> String { + iter::repeat(pad).take(len).collect::() + } - engine.register_fn("exit", || exit(0)); + let lines: Vec<_> = input.split("\n").collect(); - loop { - print!("> "); - - let mut input = String::new(); - stdout().flush().expect("couldn't flush stdout"); - - if let Err(e) = stdin().read_line(&mut input) { - println!("input error: {}", e); + // Print error + match err.position() { + p if p.is_eof() => { + // EOF + let last = lines[lines.len() - 2]; + println!("{}", last); + println!("{}^ {}", padding(" ", last.len() - 1), err); } + p if p.is_none() => { + // No position + println!("{}", err); + } + p => { + // Specific position + let pos_text = format!( + " (line {}, position {})", + p.line().unwrap(), + p.position().unwrap() + ); - if let Err(e) = engine.consume_with_scope(&mut scope, &input) { - println!("error: {}", e); + println!("{}", lines[p.line().unwrap() - 1]); + println!( + "{}^ {}", + padding(" ", p.position().unwrap() - 1), + err.to_string().replace(&pos_text, "") + ); + } + } +} + +fn main() { + let mut engine = Engine::new(); + let mut scope = Scope::new(); + + let mut input = String::new(); + + loop { + print!("rhai> "); + stdout().flush().expect("couldn't flush stdout"); + + input.clear(); + + if let Err(err) = stdin().read_line(&mut input) { + println!("input error: {}", err); + } + + if let Err(err) = engine.consume_with_scope(&mut scope, true, &input) { + println!(""); + print_error(&input, err); + println!(""); } } } diff --git a/examples/rhai_runner.rs b/examples/rhai_runner.rs index 2e0da794..d535f078 100644 --- a/examples/rhai_runner.rs +++ b/examples/rhai_runner.rs @@ -1,27 +1,79 @@ -use rhai::{Engine, RegisterFn}; -use std::env; -use std::fmt::Display; +use rhai::{Engine, EvalAltResult}; +use std::{env, fs::File, io::Read, iter, process::exit}; -fn showit(x: &mut T) -> () { - println!("{}", x) +fn padding(pad: &str, len: usize) -> String { + iter::repeat(pad).take(len).collect::() } -fn main() { - for fname in env::args().skip(1) { - let mut engine = Engine::new(); +fn eprint_error(input: &str, err: EvalAltResult) { + fn eprint_line(lines: &Vec<&str>, line: usize, pos: usize, err: &str) { + let line_no = format!("{}: ", line); + let pos_text = format!(" (line {}, position {})", line, pos); - engine.register_fn("print", showit as fn(x: &mut i32) -> ()); - engine.register_fn("print", showit as fn(x: &mut i64) -> ()); - engine.register_fn("print", showit as fn(x: &mut u32) -> ()); - engine.register_fn("print", showit as fn(x: &mut u64) -> ()); - engine.register_fn("print", showit as fn(x: &mut f32) -> ()); - engine.register_fn("print", showit as fn(x: &mut f64) -> ()); - engine.register_fn("print", showit as fn(x: &mut bool) -> ()); - engine.register_fn("print", showit as fn(x: &mut String) -> ()); + eprintln!("{}{}", line_no, lines[line - 1]); + eprintln!( + "{}^ {}", + padding(" ", line_no.len() + pos - 1), + err.replace(&pos_text, "") + ); + eprintln!(""); + } - match engine.eval_file::<()>(&fname) { - Ok(_) => (), - Err(e) => println!("Error: {}", e), + let lines: Vec<_> = input.split("\n").collect(); + + // Print error + match err.position() { + p if p.is_eof() => { + // EOF + let line = lines.len() - 1; + let pos = lines[line - 1].len(); + eprint_line(&lines, line, pos, &err.to_string()); + } + p if p.is_none() => { + // No position + eprintln!("{}", err); + } + p => { + // Specific position + eprint_line( + &lines, + p.line().unwrap(), + p.position().unwrap(), + &err.to_string(), + ) + } + } +} + +fn main() { + for filename in env::args().skip(1) { + let mut engine = Engine::new(); + + let mut f = match File::open(&filename) { + Err(err) => { + eprintln!("Error reading script file: {}\n{}", filename, err); + exit(1); + } + Ok(f) => f, + }; + + let mut contents = String::new(); + + match f.read_to_string(&mut contents) { + Err(err) => { + eprintln!("Error reading script file: {}\n{}", filename, err); + exit(1); + } + _ => (), + } + + if let Err(err) = engine.consume(&contents) { + eprintln!("{}", padding("=", filename.len())); + eprintln!("{}", filename); + eprintln!("{}", padding("=", filename.len())); + eprintln!(""); + + eprint_error(&contents, err); } } }