From e59f6939f78a31ad65d02e9eb602b44c550ff94b Mon Sep 17 00:00:00 2001 From: jhwgh1968 Date: Sat, 25 Apr 2020 18:50:10 -0500 Subject: [PATCH 01/53] Added basic Plugin trait and test --- Cargo.toml | 1 + src/fn_register.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 ++ 3 files changed, 62 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index a50bc0b0..431473e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ num-traits = { version = "0.2.11", default-features = false } [features] #default = ["no_stdlib", "no_function", "no_index", "no_object", "no_float", "only_i32", "unchecked", "no_optimize", "sync"] default = [] +plugins = [] unchecked = [] # unchecked arithmetic no_index = [] # no arrays and indexing no_float = [] # no floating-point diff --git a/src/fn_register.rs b/src/fn_register.rs index e711ee54..90289784 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -9,6 +9,49 @@ use crate::token::Position; use crate::stdlib::{any::TypeId, boxed::Box, string::ToString}; +/// A trait to register custom plugins with the `Engine`. +/// +/// A plugin consists of a number of functions. All functions will be registered with the engine. +#[cfg(feature = "plugins")] +pub trait RegisterPlugin { + /// Register a custom function with the `Engine`. + /// + /// # Example + /// + /// ``` + /// use rhai::{Dynamic, Engine, INT, Plugin, RegisterDynamicFn, RegisterPlugin}; + /// + /// // A simple custom plugin type. This should not usually be done with hand-written code. + /// struct AddOffsetPlugin(INT); + /// impl AddOffsetPlugin { + /// fn add_offset(&self, x: INT) -> Dynamic { + /// Dynamic::from(x + self.0) + /// } + /// } + /// impl Plugin for AddOffsetPlugin { + /// fn name(&self) -> &str { + /// "My Plugin" + /// } + /// fn register_contents(self, engine: &mut Engine) { + /// let add_offset_fn: Box Dynamic> = { + /// Box::new(move |x| self.add_offset(x)) + /// }; + /// engine.register_dynamic_fn("add_offset", add_offset_fn); + /// } + /// } + /// + /// # fn main() -> Result<(), Box> { + /// + /// let mut engine = Engine::new(); + /// engine.register_plugin(AddOffsetPlugin(50)); + /// + /// assert_eq!(engine.eval::("add_offset(42)")?, 92); + /// # Ok(()) + /// # } + /// ``` + fn register_plugin(&mut self, plugin: PL); +} + /// A trait to register custom functions with the `Engine`. pub trait RegisterFn { /// Register a custom function with the `Engine`. @@ -98,6 +141,15 @@ pub trait RegisterResultFn { fn register_result_fn(&mut self, name: &str, f: FN); } +/// Represents an externally-written plugin for the Rhai interpreter. +/// +/// This should not be used directly. Use the `plugin_module!` macro instead. +#[cfg(feature = "plugins")] +pub trait Plugin { + fn register_contents(self, engine: &mut Engine); + fn name(&self) -> &str; +} + // These types are used to build a unique _marker_ tuple type for each combination // of function parameter types in order to make each trait implementation unique. // That is because stable Rust currently does not allow distinguishing implementations @@ -127,6 +179,13 @@ pub fn cloned(data: &mut T) -> T { data.clone() } +#[cfg(feature = "plugins")] +impl RegisterPlugin for Engine { + fn register_plugin(&mut self, plugin: PL) { + plugin.register_contents(self) + } +} + /// This macro counts the number of arguments via recursion. macro_rules! count_args { () => { 0_usize }; diff --git a/src/lib.rs b/src/lib.rs index e13eaf0f..d442ae9a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,8 @@ pub use engine::{calc_fn_spec as calc_fn_hash, Engine}; pub use error::{ParseError, ParseErrorType}; pub use fn_call::FuncArgs; pub use fn_register::{RegisterDynamicFn, RegisterFn, RegisterResultFn}; +#[cfg(feature = "plugins")] +pub use fn_register::{Plugin, RegisterPlugin}; pub use parser::{AST, INT}; pub use result::EvalAltResult; pub use scope::Scope; From 41366d08fe3ccfe0d56ce3835d606a4520309a80 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 27 Apr 2020 22:52:20 +0800 Subject: [PATCH 02/53] Fix tests and packages for all features. --- src/packages/map_basic.rs | 6 ++---- src/packages/string_more.rs | 2 +- src/packages/time_basic.rs | 4 ++-- tests/maps.rs | 1 + 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/packages/map_basic.rs b/src/packages/map_basic.rs index 44009e8a..40e2cec0 100644 --- a/src/packages/map_basic.rs +++ b/src/packages/map_basic.rs @@ -12,12 +12,10 @@ use crate::stdlib::{ }; fn map_get_keys(map: &mut Map) -> Vec { - map.iter() - .map(|(k, _)| k.to_string().into()) - .collect::>() + map.iter().map(|(k, _)| k.to_string().into()).collect() } fn map_get_values(map: &mut Map) -> Vec { - map.iter().map(|(_, v)| v.clone()).collect::>() + map.iter().map(|(_, v)| v.clone()).collect() } #[cfg(not(feature = "no_object"))] diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 4edef10e..0144792e 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -37,7 +37,7 @@ fn sub_string(s: &mut String, start: INT, len: INT) -> String { len as usize }; - chars[offset..][..len].into_iter().collect::() + chars[offset..][..len].into_iter().collect() } fn crop_string(s: &mut String, start: INT, len: INT) { let offset = if s.is_empty() || len <= 0 { diff --git a/src/packages/time_basic.rs b/src/packages/time_basic.rs index cb0c5f8e..173c3e22 100644 --- a/src/packages/time_basic.rs +++ b/src/packages/time_basic.rs @@ -87,10 +87,10 @@ def_package!(crate:BasicTimePackage:"Basic timing utilities.", lib, { #[cfg(not(feature = "unchecked"))] { if seconds > (MAX_INT as u64) { - return Err(EvalAltResult::ErrorArithmetic( + return Err(Box::new(EvalAltResult::ErrorArithmetic( format!("Integer overflow for timestamp.elapsed(): {}", seconds), Position::none(), - )); + ))); } } return Ok(seconds as INT); diff --git a/tests/maps.rs b/tests/maps.rs index 897f8430..64c5a4c2 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -143,6 +143,7 @@ fn test_map_return() -> Result<(), Box> { } #[test] +#[cfg(not(feature = "no_index"))] fn test_map_for() -> Result<(), Box> { let engine = Engine::new(); From 29159b359b151e136c76237ebf2e9389f818c0d8 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 10 May 2020 00:13:49 +0800 Subject: [PATCH 03/53] Refactor. --- src/engine.rs | 158 ++++++++++++++++------------ src/module.rs | 80 ++++----------- src/optimize.rs | 79 +++++++------- src/parser.rs | 262 +++++++++++++++++++++++++---------------------- tests/modules.rs | 13 +-- 5 files changed, 291 insertions(+), 301 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 110b29ee..7b29129c 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -890,6 +890,8 @@ impl Engine { match rhs { // xxx.fn_name(arg_expr_list) Expr::FnCall(x) if x.1.is_none() => { + let ((name, pos), modules, hash, args, def_val) = x.as_ref(); + let mut args: Vec<_> = once(obj) .chain( idx_val @@ -898,10 +900,9 @@ impl Engine { .iter_mut(), ) .collect(); - let def_val = x.4.as_deref(); // A function call is assumed to have side effects, so the value is changed // TODO - Remove assumption of side effects by checking whether the first parameter is &mut - self.exec_fn_call(state, &x.0, x.2, &mut args, def_val, x.5, 0) + self.exec_fn_call(state, name, *hash, &mut args, def_val.as_ref(), *pos, 0) .map(|v| (v, true)) } // xxx.module::fn_name(...) - syntax error @@ -1034,17 +1035,18 @@ impl Engine { match dot_lhs { // id.??? or id[???] Expr::Variable(x) => { - let index = if state.always_search { None } else { x.3 }; - let (target, typ) = - search_scope(scope, &x.0, x.1.as_ref().map(|m| (m, x.2)), index, x.4)?; + let ((name, pos), modules, hash, index) = x.as_ref(); + let index = if state.always_search { None } else { *index }; + let mod_and_hash = modules.as_ref().map(|m| (m, *hash)); + let (target, typ) = search_scope(scope, &name, mod_and_hash, index, *pos)?; // Constants cannot be modified match typ { ScopeEntryType::Module => unreachable!(), ScopeEntryType::Constant if new_val.is_some() => { return Err(Box::new(EvalAltResult::ErrorAssignmentToConstant( - x.0.clone(), - x.4, + name.clone(), + *pos, ))); } ScopeEntryType::Constant | ScopeEntryType::Normal => (), @@ -1290,9 +1292,10 @@ impl Engine { Expr::StringConstant(x) => Ok(x.0.to_string().into()), Expr::CharConstant(x) => Ok(x.0.into()), Expr::Variable(x) => { - let index = if state.always_search { None } else { x.3 }; - let mod_and_hash = x.1.as_ref().map(|m| (m, x.2)); - let (val, _) = search_scope(scope, &x.0, mod_and_hash, index, x.4)?; + let ((name, pos), modules, hash, index) = x.as_ref(); + let index = if state.always_search { None } else { *index }; + let mod_and_hash = modules.as_ref().map(|m| (m, *hash)); + let (val, _) = search_scope(scope, name, mod_and_hash, index, *pos)?; Ok(val.clone()) } Expr::Property(_) => unreachable!(), @@ -1308,12 +1311,14 @@ impl Engine { match &x.0 { // name = rhs Expr::Variable(x) => { - let index = if state.always_search { None } else { x.3 }; - let mod_and_hash = x.1.as_ref().map(|m| (m, x.2)); - let (value_ptr, typ) = search_scope(scope, &x.0, mod_and_hash, index, x.4)?; + let ((name, pos), modules, hash, index) = x.as_ref(); + let index = if state.always_search { None } else { *index }; + let mod_and_hash = modules.as_ref().map(|m| (m, *hash)); + let (value_ptr, typ) = + search_scope(scope, name, mod_and_hash, index, *pos)?; match typ { ScopeEntryType::Constant => Err(Box::new( - EvalAltResult::ErrorAssignmentToConstant(x.0.clone(), x.4), + EvalAltResult::ErrorAssignmentToConstant(name.clone(), *pos), )), ScopeEntryType::Normal => { *value_ptr = rhs_val; @@ -1375,7 +1380,7 @@ impl Engine { #[cfg(not(feature = "no_object"))] Expr::Map(x) => Ok(Dynamic(Union::Map(Box::new( x.0.iter() - .map(|(key, expr, _)| { + .map(|((key, _), expr)| { self.eval_expr(scope, state, expr, level) .map(|val| (key.clone(), val)) }) @@ -1384,25 +1389,28 @@ impl Engine { // Normal function call Expr::FnCall(x) if x.1.is_none() => { - let mut arg_values = - x.3.iter() - .map(|expr| self.eval_expr(scope, state, expr, level)) - .collect::, _>>()?; + let ((name, pos), _, hash_fn_def, args_expr, def_val) = x.as_ref(); + + let mut arg_values = args_expr + .iter() + .map(|expr| self.eval_expr(scope, state, expr, level)) + .collect::, _>>()?; let mut args: Vec<_> = arg_values.iter_mut().collect(); let hash_fn_spec = calc_fn_hash(empty(), KEYWORD_EVAL, once(TypeId::of::())); - if x.0 == KEYWORD_EVAL + if name == KEYWORD_EVAL && args.len() == 1 - && !self.has_override(state, hash_fn_spec, x.2) + && !self.has_override(state, hash_fn_spec, *hash_fn_def) { // eval - only in function call style let prev_len = scope.len(); // Evaluate the text string as a script - let result = self.eval_script_expr(scope, state, args[0], x.3[0].position()); + let result = + self.eval_script_expr(scope, state, args[0], args_expr[0].position()); if scope.len() != prev_len { // IMPORTANT! If the eval defines new variables in the current scope, @@ -1413,20 +1421,28 @@ impl Engine { result } else { // Normal function call - except for eval (handled above) - let def_value = x.4.as_deref(); - self.exec_fn_call(state, &x.0, x.2, &mut args, def_value, x.5, level) + self.exec_fn_call( + state, + name, + *hash_fn_def, + &mut args, + def_val.as_ref(), + *pos, + level, + ) } } // Module-qualified function call #[cfg(not(feature = "no_module"))] Expr::FnCall(x) if x.1.is_some() => { - let modules = x.1.as_ref().unwrap(); + let ((name, pos), modules, hash_fn_def, args_expr, def_val) = x.as_ref(); + let modules = modules.as_ref().unwrap(); - let mut arg_values = - x.3.iter() - .map(|expr| self.eval_expr(scope, state, expr, level)) - .collect::, _>>()?; + let mut arg_values = args_expr + .iter() + .map(|expr| self.eval_expr(scope, state, expr, level)) + .collect::, _>>()?; let mut args: Vec<_> = arg_values.iter_mut().collect(); @@ -1445,8 +1461,8 @@ impl Engine { }; // First search in script-defined functions (can override built-in) - if let Some(fn_def) = module.get_qualified_scripted_fn(x.2) { - self.call_script_fn(None, state, fn_def, &mut args, x.5, level) + if let Some(fn_def) = module.get_qualified_scripted_fn(*hash_fn_def) { + self.call_script_fn(None, state, fn_def, &mut args, *pos, level) } else { // Then search in Rust functions @@ -1455,13 +1471,13 @@ impl Engine { // i.e. qualifiers + function name + dummy parameter types (one for each parameter). // 2) Calculate a second hash with no qualifiers, empty function name, and // the actual list of parameter `TypeId`'.s - let hash2 = calc_fn_hash(empty(), "", args.iter().map(|a| a.type_id())); + let hash_fn_args = calc_fn_hash(empty(), "", args.iter().map(|a| a.type_id())); // 3) The final hash is the XOR of the two hashes. - let hash = x.2 ^ hash2; + let hash = *hash_fn_def ^ hash_fn_args; - match module.get_qualified_fn(&x.0, hash, x.5) { - Ok(func) => func(&mut args, x.5), - Err(_) if x.4.is_some() => Ok(x.4.as_deref().unwrap().clone()), + match module.get_qualified_fn(name, hash, *pos) { + Ok(func) => func(&mut args, *pos), + Err(_) if def_val.is_some() => Ok(def_val.clone().unwrap()), Err(err) => Err(err), } } @@ -1469,35 +1485,41 @@ impl Engine { Expr::In(x) => self.eval_in_expr(scope, state, &x.0, &x.1, level), - Expr::And(x) => Ok((self - .eval_expr(scope, state, &x.0, level)? + Expr::And(x) => { + let (lhs, rhs, _) = x.as_ref(); + Ok((self + .eval_expr(scope, state, lhs, level)? .as_bool() .map_err(|_| { - EvalAltResult::ErrorBooleanArgMismatch("AND".into(), x.0.position()) + EvalAltResult::ErrorBooleanArgMismatch("AND".into(), lhs.position()) })? && // Short-circuit using && self - .eval_expr(scope, state, &x.1, level)? + .eval_expr(scope, state, rhs, level)? .as_bool() .map_err(|_| { - EvalAltResult::ErrorBooleanArgMismatch("AND".into(), x.1.position()) + EvalAltResult::ErrorBooleanArgMismatch("AND".into(), rhs.position()) })?) - .into()), + .into()) + } - Expr::Or(x) => Ok((self - .eval_expr(scope, state, &x.0, level)? + Expr::Or(x) => { + let (lhs, rhs, _) = x.as_ref(); + Ok((self + .eval_expr(scope, state, lhs, level)? .as_bool() .map_err(|_| { - EvalAltResult::ErrorBooleanArgMismatch("OR".into(), x.0.position()) + EvalAltResult::ErrorBooleanArgMismatch("OR".into(), lhs.position()) })? || // Short-circuit using || self - .eval_expr(scope, state, &x.1, level)? + .eval_expr(scope, state, rhs, level)? .as_bool() .map_err(|_| { - EvalAltResult::ErrorBooleanArgMismatch("OR".into(), x.1.position()) + EvalAltResult::ErrorBooleanArgMismatch("OR".into(), rhs.position()) })?) - .into()), + .into()) + } Expr::True(_) => Ok(true.into()), Expr::False(_) => Ok(false.into()), @@ -1633,56 +1655,56 @@ impl Engine { Stmt::Break(pos) => Err(Box::new(EvalAltResult::ErrorLoopBreak(true, *pos))), // Return value - Stmt::ReturnWithVal(x) if x.0.is_some() && x.1 == ReturnType::Return => { + Stmt::ReturnWithVal(x) if x.1.is_some() && (x.0).0 == ReturnType::Return => { Err(Box::new(EvalAltResult::Return( - self.eval_expr(scope, state, x.0.as_ref().unwrap(), level)?, - x.2, + self.eval_expr(scope, state, x.1.as_ref().unwrap(), level)?, + (x.0).1, ))) } // Empty return - Stmt::ReturnWithVal(x) if x.1 == ReturnType::Return => { - Err(Box::new(EvalAltResult::Return(Default::default(), x.2))) + Stmt::ReturnWithVal(x) if (x.0).0 == ReturnType::Return => { + Err(Box::new(EvalAltResult::Return(Default::default(), (x.0).1))) } // Throw value - Stmt::ReturnWithVal(x) if x.0.is_some() && x.1 == ReturnType::Exception => { - let val = self.eval_expr(scope, state, x.0.as_ref().unwrap(), level)?; + Stmt::ReturnWithVal(x) if x.1.is_some() && (x.0).0 == ReturnType::Exception => { + let val = self.eval_expr(scope, state, x.1.as_ref().unwrap(), level)?; Err(Box::new(EvalAltResult::ErrorRuntime( val.take_string().unwrap_or_else(|_| "".to_string()), - x.2, + (x.0).1, ))) } // Empty throw - Stmt::ReturnWithVal(x) if x.1 == ReturnType::Exception => { - Err(Box::new(EvalAltResult::ErrorRuntime("".into(), x.2))) + Stmt::ReturnWithVal(x) if (x.0).0 == ReturnType::Exception => { + Err(Box::new(EvalAltResult::ErrorRuntime("".into(), (x.0).1))) } Stmt::ReturnWithVal(_) => unreachable!(), // Let statement Stmt::Let(x) if x.1.is_some() => { - let val = self.eval_expr(scope, state, x.1.as_ref().unwrap(), level)?; + let ((var_name, _), expr) = x.as_ref(); + let val = self.eval_expr(scope, state, expr.as_ref().unwrap(), level)?; // TODO - avoid copying variable name in inner block? - let var_name = x.0.clone(); - scope.push_dynamic_value(var_name, ScopeEntryType::Normal, val, false); + scope.push_dynamic_value(var_name.clone(), ScopeEntryType::Normal, val, false); Ok(Default::default()) } Stmt::Let(x) => { + let ((var_name, _), _) = x.as_ref(); // TODO - avoid copying variable name in inner block? - let var_name = x.0.clone(); - scope.push(var_name, ()); + scope.push(var_name.clone(), ()); Ok(Default::default()) } // Const statement Stmt::Const(x) if x.1.is_constant() => { - let val = self.eval_expr(scope, state, &x.1, level)?; + let ((var_name, _), expr) = x.as_ref(); + let val = self.eval_expr(scope, state, &expr, level)?; // TODO - avoid copying variable name in inner block? - let var_name = x.0.clone(); - scope.push_dynamic_value(var_name, ScopeEntryType::Constant, val, true); + scope.push_dynamic_value(var_name.clone(), ScopeEntryType::Constant, val, true); Ok(Default::default()) } @@ -1691,7 +1713,7 @@ impl Engine { // Import statement Stmt::Import(x) => { - let (expr, name, _) = x.as_ref(); + let (expr, (name, _)) = x.as_ref(); #[cfg(feature = "no_module")] unreachable!(); @@ -1725,7 +1747,7 @@ impl Engine { // Export statement Stmt::Export(list) => { - for (id, id_pos, rename) in list.as_ref() { + for ((id, id_pos), rename) in list.as_ref() { let mut found = false; // Mark scope variables as public diff --git a/src/module.rs b/src/module.rs index 75c2b126..0519a44b 100644 --- a/src/module.rs +++ b/src/module.rs @@ -250,7 +250,7 @@ impl Module { /// use rhai::Module; /// /// let mut module = Module::new(); - /// let hash = module.set_fn_0("calc", || Ok(42_i64), false); + /// let hash = module.set_fn_0("calc", || Ok(42_i64)); /// assert!(module.contains_fn(hash)); /// ``` pub fn contains_fn(&self, hash: u64) -> bool { @@ -289,7 +289,7 @@ impl Module { /// use rhai::Module; /// /// let mut module = Module::new(); - /// let hash = module.set_fn_0("calc", || Ok(42_i64), false); + /// let hash = module.set_fn_0("calc", || Ok(42_i64)); /// assert!(module.get_fn(hash).is_some()); /// ``` pub fn set_fn_0, T: Into>( @@ -297,7 +297,6 @@ impl Module { fn_name: K, #[cfg(not(feature = "sync"))] func: impl Fn() -> FuncReturn + 'static, #[cfg(feature = "sync")] func: impl Fn() -> FuncReturn + Send + Sync + 'static, - is_private: bool, ) -> u64 { let f = move |_: &mut FnCallArgs, pos| { func() @@ -305,12 +304,7 @@ impl Module { .map_err(|err| EvalAltResult::set_position(err, pos)) }; let arg_types = vec![]; - let access = if is_private { - FnAccess::Private - } else { - FnAccess::Public - }; - self.set_fn(fn_name.into(), access, arg_types, Box::new(f)) + self.set_fn(fn_name.into(), FnAccess::Public, arg_types, Box::new(f)) } /// Set a Rust function taking one parameter into the module, returning a hash key. @@ -323,7 +317,7 @@ impl Module { /// use rhai::Module; /// /// let mut module = Module::new(); - /// let hash = module.set_fn_1("calc", |x: i64| Ok(x + 1), false); + /// let hash = module.set_fn_1("calc", |x: i64| Ok(x + 1)); /// assert!(module.get_fn(hash).is_some()); /// ``` pub fn set_fn_1, A: Variant + Clone, T: Into>( @@ -331,7 +325,6 @@ impl Module { fn_name: K, #[cfg(not(feature = "sync"))] func: impl Fn(A) -> FuncReturn + 'static, #[cfg(feature = "sync")] func: impl Fn(A) -> FuncReturn + Send + Sync + 'static, - is_private: bool, ) -> u64 { let f = move |args: &mut FnCallArgs, pos| { func(mem::take(args[0]).cast::()) @@ -339,12 +332,7 @@ impl Module { .map_err(|err| EvalAltResult::set_position(err, pos)) }; let arg_types = vec![TypeId::of::()]; - let access = if is_private { - FnAccess::Private - } else { - FnAccess::Public - }; - self.set_fn(fn_name.into(), access, arg_types, Box::new(f)) + self.set_fn(fn_name.into(), FnAccess::Public, arg_types, Box::new(f)) } /// Set a Rust function taking one mutable parameter into the module, returning a hash key. @@ -357,7 +345,7 @@ impl Module { /// use rhai::Module; /// /// let mut module = Module::new(); - /// let hash = module.set_fn_1_mut("calc", |x: &mut i64| { *x += 1; Ok(*x) }, false); + /// let hash = module.set_fn_1_mut("calc", |x: &mut i64| { *x += 1; Ok(*x) }); /// assert!(module.get_fn(hash).is_some()); /// ``` pub fn set_fn_1_mut, A: Variant + Clone, T: Into>( @@ -365,7 +353,6 @@ impl Module { fn_name: K, #[cfg(not(feature = "sync"))] func: impl Fn(&mut A) -> FuncReturn + 'static, #[cfg(feature = "sync")] func: impl Fn(&mut A) -> FuncReturn + Send + Sync + 'static, - is_private: bool, ) -> u64 { let f = move |args: &mut FnCallArgs, pos| { func(args[0].downcast_mut::().unwrap()) @@ -373,12 +360,7 @@ impl Module { .map_err(|err| EvalAltResult::set_position(err, pos)) }; let arg_types = vec![TypeId::of::()]; - let access = if is_private { - FnAccess::Private - } else { - FnAccess::Public - }; - self.set_fn(fn_name.into(), access, arg_types, Box::new(f)) + self.set_fn(fn_name.into(), FnAccess::Public, arg_types, Box::new(f)) } /// Set a Rust function taking two parameters into the module, returning a hash key. @@ -393,7 +375,7 @@ impl Module { /// let mut module = Module::new(); /// let hash = module.set_fn_2("calc", |x: i64, y: String| { /// Ok(x + y.len() as i64) - /// }, false); + /// }); /// assert!(module.get_fn(hash).is_some()); /// ``` pub fn set_fn_2, A: Variant + Clone, B: Variant + Clone, T: Into>( @@ -401,7 +383,6 @@ impl Module { fn_name: K, #[cfg(not(feature = "sync"))] func: impl Fn(A, B) -> FuncReturn + 'static, #[cfg(feature = "sync")] func: impl Fn(A, B) -> FuncReturn + Send + Sync + 'static, - is_private: bool, ) -> u64 { let f = move |args: &mut FnCallArgs, pos| { let a = mem::take(args[0]).cast::(); @@ -412,12 +393,7 @@ impl Module { .map_err(|err| EvalAltResult::set_position(err, pos)) }; let arg_types = vec![TypeId::of::(), TypeId::of::()]; - let access = if is_private { - FnAccess::Private - } else { - FnAccess::Public - }; - self.set_fn(fn_name.into(), access, arg_types, Box::new(f)) + self.set_fn(fn_name.into(), FnAccess::Public, arg_types, Box::new(f)) } /// Set a Rust function taking two parameters (the first one mutable) into the module, @@ -431,7 +407,7 @@ impl Module { /// let mut module = Module::new(); /// let hash = module.set_fn_2_mut("calc", |x: &mut i64, y: String| { /// *x += y.len() as i64; Ok(*x) - /// }, false); + /// }); /// assert!(module.get_fn(hash).is_some()); /// ``` pub fn set_fn_2_mut< @@ -444,7 +420,6 @@ impl Module { fn_name: K, #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B) -> FuncReturn + 'static, #[cfg(feature = "sync")] func: impl Fn(&mut A, B) -> FuncReturn + Send + Sync + 'static, - is_private: bool, ) -> u64 { let f = move |args: &mut FnCallArgs, pos| { let b = mem::take(args[1]).cast::(); @@ -455,12 +430,7 @@ impl Module { .map_err(|err| EvalAltResult::set_position(err, pos)) }; let arg_types = vec![TypeId::of::(), TypeId::of::()]; - let access = if is_private { - FnAccess::Private - } else { - FnAccess::Public - }; - self.set_fn(fn_name.into(), access, arg_types, Box::new(f)) + self.set_fn(fn_name.into(), FnAccess::Public, arg_types, Box::new(f)) } /// Set a Rust function taking three parameters into the module, returning a hash key. @@ -475,7 +445,7 @@ impl Module { /// let mut module = Module::new(); /// let hash = module.set_fn_3("calc", |x: i64, y: String, z: i64| { /// Ok(x + y.len() as i64 + z) - /// }, false); + /// }); /// assert!(module.get_fn(hash).is_some()); /// ``` pub fn set_fn_3< @@ -489,7 +459,6 @@ impl Module { fn_name: K, #[cfg(not(feature = "sync"))] func: impl Fn(A, B, C) -> FuncReturn + 'static, #[cfg(feature = "sync")] func: impl Fn(A, B, C) -> FuncReturn + Send + Sync + 'static, - is_private: bool, ) -> u64 { let f = move |args: &mut FnCallArgs, pos| { let a = mem::take(args[0]).cast::(); @@ -501,12 +470,7 @@ impl Module { .map_err(|err| EvalAltResult::set_position(err, pos)) }; let arg_types = vec![TypeId::of::(), TypeId::of::(), TypeId::of::()]; - let access = if is_private { - FnAccess::Private - } else { - FnAccess::Public - }; - self.set_fn(fn_name.into(), access, arg_types, Box::new(f)) + self.set_fn(fn_name.into(), FnAccess::Public, arg_types, Box::new(f)) } /// Set a Rust function taking three parameters (the first one mutable) into the module, @@ -522,7 +486,7 @@ impl Module { /// let mut module = Module::new(); /// let hash = module.set_fn_3_mut("calc", |x: &mut i64, y: String, z: i64| { /// *x += y.len() as i64 + z; Ok(*x) - /// }, false); + /// }); /// assert!(module.get_fn(hash).is_some()); /// ``` pub fn set_fn_3_mut< @@ -536,7 +500,6 @@ impl Module { fn_name: K, #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B, C) -> FuncReturn + 'static, #[cfg(feature = "sync")] func: impl Fn(&mut A, B, C) -> FuncReturn + Send + Sync + 'static, - is_private: bool, ) -> u64 { let f = move |args: &mut FnCallArgs, pos| { let b = mem::take(args[1]).cast::(); @@ -548,12 +511,7 @@ impl Module { .map_err(|err| EvalAltResult::set_position(err, pos)) }; let arg_types = vec![TypeId::of::(), TypeId::of::(), TypeId::of::()]; - let access = if is_private { - FnAccess::Private - } else { - FnAccess::Public - }; - self.set_fn(fn_name.into(), access, arg_types, Box::new(f)) + self.set_fn(fn_name.into(), FnAccess::Public, arg_types, Box::new(f)) } /// Get a Rust function. @@ -567,7 +525,7 @@ impl Module { /// use rhai::Module; /// /// let mut module = Module::new(); - /// let hash = module.set_fn_1("calc", |x: i64| Ok(x + 1), false); + /// let hash = module.set_fn_1("calc", |x: i64| Ok(x + 1)); /// assert!(module.get_fn(hash).is_some()); /// ``` pub fn get_fn(&self, hash: u64) -> Option<&Box> { @@ -694,16 +652,16 @@ impl Module { // Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, // i.e. qualifiers + function name + dummy parameter types (one for each parameter). - let hash1 = calc_fn_hash( + let hash_fn_def = calc_fn_hash( qualifiers.iter().map(|v| *v), fn_name, repeat(EMPTY_TYPE_ID()).take(params.len()), ); // 2) Calculate a second hash with no qualifiers, empty function name, and // the actual list of parameter `TypeId`'.s - let hash2 = calc_fn_hash(empty(), "", params.iter().cloned()); + let hash_fn_args = calc_fn_hash(empty(), "", params.iter().cloned()); // 3) The final hash is the XOR of the two hashes. - let hash = hash1 ^ hash2; + let hash = hash_fn_def ^ hash_fn_args; functions.push((hash, func.clone())); } diff --git a/src/optimize.rs b/src/optimize.rs index 223949d6..09f294c6 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -220,15 +220,13 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - optimize_stmt(x.2, state, false), ))), // let id = expr; - Stmt::Let(x) if x.1.is_some() => Stmt::Let(Box::new(( - x.0, - Some(optimize_expr(x.1.unwrap(), state)), - x.2, - ))), + Stmt::Let(x) if x.1.is_some() => { + Stmt::Let(Box::new((x.0, Some(optimize_expr(x.1.unwrap(), state))))) + } // let id; stmt @ Stmt::Let(_) => stmt, // import expr as id; - Stmt::Import(x) => Stmt::Import(Box::new((optimize_expr(x.0, state), x.1, x.2))), + Stmt::Import(x) => Stmt::Import(Box::new((optimize_expr(x.0, state), x.1))), // { block } Stmt::Block(x) => { let orig_len = x.0.len(); // Original number of statements in the block, for change detection @@ -241,9 +239,10 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - .map(|stmt| match stmt { // Add constant into the state Stmt::Const(v) => { - state.push_constant(&v.0, v.1); + let ((name, pos), expr) = *v; + state.push_constant(&name, expr); state.set_dirty(); - Stmt::Noop(v.2) // No need to keep constants + Stmt::Noop(pos) // No need to keep constants } // Optimize the statement _ => optimize_stmt(stmt, state, preserve_result), @@ -336,11 +335,9 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - // expr; Stmt::Expr(expr) => Stmt::Expr(Box::new(optimize_expr(*expr, state))), // return expr; - Stmt::ReturnWithVal(x) if x.0.is_some() => Stmt::ReturnWithVal(Box::new(( - Some(optimize_expr(x.0.unwrap(), state)), - x.1, - x.2, - ))), + Stmt::ReturnWithVal(x) if x.1.is_some() => { + Stmt::ReturnWithVal(Box::new((x.0, Some(optimize_expr(x.1.unwrap(), state))))) + } // All other statements - skip stmt => stmt, } @@ -394,13 +391,13 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { #[cfg(not(feature = "no_object"))] Expr::Dot(x) => match (x.0, x.1) { // map.string - (Expr::Map(m), Expr::Property(p)) if m.0.iter().all(|(_, x, _)| x.is_pure()) => { + (Expr::Map(m), Expr::Property(p)) if m.0.iter().all(|(_, x)| x.is_pure()) => { // Map literal where everything is pure - promote the indexed item. // All other items can be thrown away. state.set_dirty(); let pos = m.1; - m.0.into_iter().find(|(name, _, _)| name == &p.0) - .map(|(_, expr, _)| expr.set_position(pos)) + m.0.into_iter().find(|((name, _), _)| name == &p.0) + .map(|(_, expr)| expr.set_position(pos)) .unwrap_or_else(|| Expr::Unit(pos)) } // lhs.rhs @@ -420,13 +417,13 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { a.0.remove(i.0 as usize).set_position(a.1) } // map[string] - (Expr::Map(m), Expr::StringConstant(s)) if m.0.iter().all(|(_, x, _)| x.is_pure()) => { + (Expr::Map(m), Expr::StringConstant(s)) if m.0.iter().all(|(_, x)| x.is_pure()) => { // Map literal where everything is pure - promote the indexed item. // All other items can be thrown away. state.set_dirty(); let pos = m.1; - m.0.into_iter().find(|(name, _, _)| name == &s.0) - .map(|(_, expr, _)| expr.set_position(pos)) + m.0.into_iter().find(|((name, _), _)| name == &s.0) + .map(|(_, expr)| expr.set_position(pos)) .unwrap_or_else(|| Expr::Unit(pos)) } // string[int] @@ -448,7 +445,7 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { #[cfg(not(feature = "no_object"))] Expr::Map(m) => Expr::Map(Box::new((m.0 .into_iter() - .map(|(key, expr, pos)| (key, optimize_expr(expr, state), pos)) + .map(|((key, pos), expr)| ((key, pos), optimize_expr(expr, state))) .collect(), m.1))), // lhs in rhs Expr::In(x) => match (x.0, x.1) { @@ -465,7 +462,7 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { // "xxx" in #{...} (Expr::StringConstant(a), Expr::Map(b)) => { state.set_dirty(); - if b.0.iter().find(|(name, _, _)| name == &a.0).is_some() { + if b.0.iter().find(|((name, _), _)| name == &a.0).is_some() { Expr::True(a.1) } else { Expr::False(a.1) @@ -476,7 +473,7 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { state.set_dirty(); let ch = a.0.to_string(); - if b.0.iter().find(|(name, _, _)| name == &ch).is_some() { + if b.0.iter().find(|((name, _), _)| name == &ch).is_some() { Expr::True(a.1) } else { Expr::False(a.1) @@ -527,7 +524,7 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { }, // Do not call some special keywords - Expr::FnCall(mut x) if DONT_EVAL_KEYWORDS.contains(&x.0.as_ref().as_ref())=> { + Expr::FnCall(mut x) if DONT_EVAL_KEYWORDS.contains(&(x.0).0.as_ref())=> { x.3 = x.3.into_iter().map(|a| optimize_expr(a, state)).collect(); Expr::FnCall(x) } @@ -538,25 +535,27 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { && state.optimization_level == OptimizationLevel::Full // full optimizations && x.3.iter().all(|expr| expr.is_constant()) // all arguments are constants => { + let ((name, pos), _, _, args, def_value) = x.as_mut(); + // First search in script-defined functions (can override built-in) - if state.fn_lib.iter().find(|(name, len)| *name == x.0 && *len == x.3.len()).is_some() { + if state.fn_lib.iter().find(|(id, len)| *id == name && *len == args.len()).is_some() { // A script-defined function overrides the built-in function - do not make the call x.3 = x.3.into_iter().map(|a| optimize_expr(a, state)).collect(); return Expr::FnCall(x); } - let mut arg_values: Vec<_> = x.3.iter().map(Expr::get_constant_value).collect(); + let mut arg_values: Vec<_> = args.iter().map(Expr::get_constant_value).collect(); let mut call_args: Vec<_> = arg_values.iter_mut().collect(); // Save the typename of the first argument if it is `type_of()` // This is to avoid `call_args` being passed into the closure - let arg_for_type_of = if x.0 == KEYWORD_TYPE_OF && call_args.len() == 1 { + let arg_for_type_of = if name == KEYWORD_TYPE_OF && call_args.len() == 1 { state.engine.map_type_name(call_args[0].type_name()) } else { "" }; - call_fn(&state.engine.packages, &state.engine.base_package, &x.0, &mut call_args, x.5).ok() + call_fn(&state.engine.packages, &state.engine.base_package, name, &mut call_args, *pos).ok() .and_then(|result| result.or_else(|| { if !arg_for_type_of.is_empty() { @@ -564,9 +563,9 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { Some(arg_for_type_of.to_string().into()) } else { // Otherwise use the default value, if any - x.4.clone().map(|v| *v) + def_value.clone() } - }).and_then(|result| map_dynamic_to_expr(result, x.5)) + }).and_then(|result| map_dynamic_to_expr(result, *pos)) .map(|expr| { state.set_dirty(); expr @@ -585,11 +584,12 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { } // constant-name - Expr::Variable(x) if x.1.is_none() && state.contains_constant(&x.0) => { + Expr::Variable(x) if x.1.is_none() && state.contains_constant(&(x.0).0) => { + let (name, pos) = x.0; state.set_dirty(); // Replace constant with value - state.find_constant(&x.0).expect("should find constant in scope!").clone().set_position(x.4) + state.find_constant(&name).expect("should find constant in scope!").clone().set_position(pos) } // All other expressions - skip @@ -643,9 +643,10 @@ fn optimize<'a>( .enumerate() .map(|(i, stmt)| { match &stmt { - Stmt::Const(x) => { + Stmt::Const(v) => { // Load constants - state.push_constant(&x.0, x.1.clone()); + let ((name, _), expr) = v.as_ref(); + state.push_constant(&name, expr.clone()); stmt // Keep it in the global scope } _ => { @@ -718,12 +719,16 @@ pub fn optimize_into_ast( // {} -> Noop fn_def.body = match body.pop().unwrap_or_else(|| Stmt::Noop(pos)) { // { return val; } -> val - Stmt::ReturnWithVal(x) if x.0.is_some() && x.1 == ReturnType::Return => { - Stmt::Expr(Box::new(x.0.unwrap())) + Stmt::ReturnWithVal(x) + if x.1.is_some() && (x.0).0 == ReturnType::Return => + { + Stmt::Expr(Box::new(x.1.unwrap())) } // { return; } -> () - Stmt::ReturnWithVal(x) if x.0.is_none() && x.1 == ReturnType::Return => { - Stmt::Expr(Box::new(Expr::Unit(x.2))) + Stmt::ReturnWithVal(x) + if x.1.is_none() && (x.0).0 == ReturnType::Return => + { + Stmt::Expr(Box::new(Expr::Unit((x.0).1))) } // All others stmt => stmt, diff --git a/src/parser.rs b/src/parser.rs index 7a88abfc..8041dcef 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -280,9 +280,9 @@ pub enum Stmt { /// for id in expr { stmt } For(Box<(String, Expr, Stmt)>), /// let id = expr - Let(Box<(String, Option, Position)>), + Let(Box<((String, Position), Option)>), /// const id = expr - Const(Box<(String, Expr, Position)>), + Const(Box<((String, Position), Expr)>), /// { stmt; ... } Block(Box<(Vec, Position)>), /// { stmt } @@ -292,11 +292,11 @@ pub enum Stmt { /// break Break(Position), /// return/throw - ReturnWithVal(Box<(Option, ReturnType, Position)>), + ReturnWithVal(Box<((ReturnType, Position), Option)>), /// import expr as module - Import(Box<(Expr, String, Position)>), + Import(Box<(Expr, (String, Position))>), /// expr id as name, ... - Export(Box)>>), + Export(Box)>>), } impl Stmt { @@ -304,17 +304,17 @@ impl Stmt { pub fn position(&self) -> Position { match self { Stmt::Noop(pos) | Stmt::Continue(pos) | Stmt::Break(pos) => *pos, - Stmt::Let(x) => x.2, - Stmt::Const(x) => x.2, - Stmt::ReturnWithVal(x) => x.2, + Stmt::Let(x) => (x.0).1, + Stmt::Const(x) => (x.0).1, + Stmt::ReturnWithVal(x) => (x.0).1, Stmt::Block(x) => x.1, Stmt::IfThenElse(x) => x.0.position(), Stmt::Expr(x) => x.position(), Stmt::While(x) => x.1.position(), Stmt::Loop(x) => x.position(), Stmt::For(x) => x.2.position(), - Stmt::Import(x) => x.2, - Stmt::Export(x) => x.get(0).unwrap().1, + Stmt::Import(x) => (x.1).1, + Stmt::Export(x) => (x.get(0).unwrap().0).1, } } @@ -379,23 +379,22 @@ pub enum Expr { CharConstant(Box<(char, Position)>), /// String constant. StringConstant(Box<(String, Position)>), - /// Variable access - (variable name, optional modules, hash, optional index, position) - Variable(Box<(String, MRef, u64, Option, Position)>), + /// Variable access - ((variable name, position), optional modules, hash, optional index) + Variable(Box<((String, Position), MRef, u64, Option)>), /// Property access. Property(Box<(String, Position)>), /// { stmt } Stmt(Box<(Stmt, Position)>), - /// func(expr, ... ) - (function name, optional modules, hash, arguments, optional default value, position) + /// func(expr, ... ) - ((function name, position), optional modules, hash, arguments, optional default value) /// Use `Cow<'static, str>` because a lot of operators (e.g. `==`, `>=`) are implemented as function calls /// and the function names are predictable, so no need to allocate a new `String`. FnCall( Box<( - Cow<'static, str>, + (Cow<'static, str>, Position), MRef, u64, Vec, - Option>, - Position, + Option, )>, ), /// expr = expr @@ -407,7 +406,7 @@ pub enum Expr { /// [ expr, ... ] Array(Box<(Vec, Position)>), /// #{ name:expr, ... } - Map(Box<(Vec<(String, Expr, Position)>, Position)>), + Map(Box<(Vec<((String, Position), Expr)>, Position)>), /// lhs in rhs In(Box<(Expr, Expr, Position)>), /// lhs && rhs @@ -445,10 +444,10 @@ impl Expr { ))), #[cfg(not(feature = "no_object"))] - Self::Map(x) if x.0.iter().all(|(_, v, _)| v.is_constant()) => { + Self::Map(x) if x.0.iter().all(|(_, v)| v.is_constant()) => { Dynamic(Union::Map(Box::new( x.0.iter() - .map(|(k, v, _)| (k.clone(), v.get_constant_value())) + .map(|((k, _), v)| (k.clone(), v.get_constant_value())) .collect::>(), ))) } @@ -493,8 +492,8 @@ impl Expr { Self::Map(x) => x.1, Self::Property(x) => x.1, Self::Stmt(x) => x.1, - Self::Variable(x) => x.4, - Self::FnCall(x) => x.5, + Self::Variable(x) => (x.0).1, + Self::FnCall(x) => (x.0).1, Self::And(x) | Self::Or(x) | Self::In(x) => x.2, @@ -515,10 +514,10 @@ impl Expr { Self::StringConstant(x) => x.1 = new_pos, Self::Array(x) => x.1 = new_pos, Self::Map(x) => x.1 = new_pos, - Self::Variable(x) => x.4 = new_pos, + Self::Variable(x) => (x.0).1 = new_pos, Self::Property(x) => x.1 = new_pos, Self::Stmt(x) => x.1 = new_pos, - Self::FnCall(x) => x.5 = new_pos, + Self::FnCall(x) => (x.0).1 = new_pos, Self::And(x) => x.2 = new_pos, Self::Or(x) => x.2 = new_pos, Self::In(x) => x.2 = new_pos, @@ -541,7 +540,8 @@ impl Expr { Self::Array(x) => x.0.iter().all(Self::is_pure), Self::Index(x) | Self::And(x) | Self::Or(x) | Self::In(x) => { - x.0.is_pure() && x.1.is_pure() + let (lhs, rhs, _) = x.as_ref(); + lhs.is_pure() && rhs.is_pure() } Self::Stmt(x) => x.0.is_pure(), @@ -569,7 +569,7 @@ impl Expr { Self::Array(x) => x.0.iter().all(Self::is_constant), // An map literal is constant if all items are constant - Self::Map(x) => x.0.iter().map(|(_, expr, _)| expr).all(Self::is_constant), + Self::Map(x) => x.0.iter().map(|(_, expr)| expr).all(Self::is_constant), // Check in expression Self::In(x) => match (&x.0, &x.1) { @@ -626,7 +626,10 @@ impl Expr { /// Convert a `Variable` into a `Property`. All other variants are untouched. pub(crate) fn into_property(self) -> Self { match self { - Self::Variable(x) if x.1.is_none() => Self::Property(Box::new((x.0.clone(), x.4))), + Self::Variable(x) if x.1.is_none() => { + let (name, pos) = x.0; + Self::Property(Box::new((name.clone(), pos))) + } _ => self, } } @@ -713,34 +716,31 @@ fn parse_call_expr<'a>( eat_token(input, Token::RightParen); #[cfg(not(feature = "no_module"))] - let hash1 = { + let hash_fn_def = { if let Some(modules) = modules.as_mut() { modules.set_index(stack.find_module(&modules.get(0).0)); // Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, // i.e. qualifiers + function name + no parameters. - let hash1 = calc_fn_hash(modules.iter().map(|(m, _)| m.as_str()), &id, empty()); // 2) Calculate a second hash with no qualifiers, empty function name, and // the actual list of parameter `TypeId`'.s // 3) The final hash is the XOR of the two hashes. - - hash1 + calc_fn_hash(modules.iter().map(|(m, _)| m.as_str()), &id, empty()) } else { calc_fn_hash(empty(), &id, empty()) } }; // Qualifiers (none) + function name + no parameters. #[cfg(feature = "no_module")] - let hash1 = calc_fn_hash(empty(), &id, empty()); + let hash_fn_def = calc_fn_hash(empty(), &id, empty()); return Ok(Expr::FnCall(Box::new(( - id.into(), + (id.into(), begin), modules, - hash1, + hash_fn_def, args, None, - begin, )))); } // id... @@ -756,38 +756,36 @@ fn parse_call_expr<'a>( eat_token(input, Token::RightParen); #[cfg(not(feature = "no_module"))] - let hash1 = { + let hash_fn_def = { if let Some(modules) = modules.as_mut() { modules.set_index(stack.find_module(&modules.get(0).0)); // Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, // i.e. qualifiers + function name + dummy parameter types (one for each parameter). - let hash1 = calc_fn_hash( - modules.iter().map(|(m, _)| m.as_str()), - &id, - repeat(EMPTY_TYPE_ID()).take(args.len()), - ); // 2) Calculate a second hash with no qualifiers, empty function name, and // the actual list of parameter `TypeId`'.s // 3) The final hash is the XOR of the two hashes. - - hash1 + calc_fn_hash( + modules.iter().map(|(m, _)| m.as_str()), + &id, + repeat(EMPTY_TYPE_ID()).take(args.len()), + ) } else { calc_fn_hash(empty(), &id, repeat(EMPTY_TYPE_ID()).take(args.len())) } }; // Qualifiers (none) + function name + dummy parameter types (one for each parameter). #[cfg(feature = "no_module")] - let hash1 = calc_fn_hash(empty(), &id, repeat(EMPTY_TYPE_ID()).take(args.len())); + let hash_fn_def = + calc_fn_hash(empty(), &id, repeat(EMPTY_TYPE_ID()).take(args.len())); return Ok(Expr::FnCall(Box::new(( - id.into(), + (id.into(), begin), modules, - hash1, + hash_fn_def, args, None, - begin, )))); } // id(...args, @@ -1094,7 +1092,7 @@ fn parse_map_literal<'a>( let expr = parse_expr(input, stack, allow_stmt_expr)?; - map.push((name, expr, pos)); + map.push(((name, pos), expr)); match input.peek().unwrap() { (Token::Comma, _) => { @@ -1127,11 +1125,11 @@ fn parse_map_literal<'a>( // Check for duplicating properties map.iter() .enumerate() - .try_for_each(|(i, (k1, _, _))| { + .try_for_each(|(i, ((k1, _), _))| { map.iter() .skip(i + 1) - .find(|(k2, _, _)| k2 == k1) - .map_or_else(|| Ok(()), |(k2, _, pos)| Err((k2, *pos))) + .find(|((k2, _), _)| k2 == k1) + .map_or_else(|| Ok(()), |((k2, pos), _)| Err((k2, *pos))) }) .map_err(|(key, pos)| PERR::DuplicatedProperty(key.to_string()).into_err(pos))?; @@ -1163,7 +1161,7 @@ fn parse_primary<'a>( Token::StringConst(s) => Expr::StringConstant(Box::new((s, pos))), Token::Identifier(s) => { let index = stack.find(&s); - Expr::Variable(Box::new((s, None, 0, index, pos))) + Expr::Variable(Box::new(((s, pos), None, 0, index))) } Token::LeftParen => parse_paren_expr(input, stack, pos, allow_stmt_expr)?, #[cfg(not(feature = "no_index"))] @@ -1191,24 +1189,27 @@ fn parse_primary<'a>( root_expr = match (root_expr, token) { // Function call (Expr::Variable(x), Token::LeftParen) => { - parse_call_expr(input, stack, x.0, x.1, x.4, allow_stmt_expr)? + let ((name, pos), modules, _, _) = *x; + parse_call_expr(input, stack, name, modules, pos, allow_stmt_expr)? } (Expr::Property(x), Token::LeftParen) => { - parse_call_expr(input, stack, x.0, None, x.1, allow_stmt_expr)? + let (name, pos) = *x; + parse_call_expr(input, stack, name, None, pos, allow_stmt_expr)? } // module access #[cfg(not(feature = "no_module"))] - (Expr::Variable(mut x), Token::DoubleColon) => match input.next().unwrap() { + (Expr::Variable(x), Token::DoubleColon) => match input.next().unwrap() { (Token::Identifier(id2), pos2) => { - if let Some(ref mut modules) = x.1 { - modules.push((x.0, x.4)); + let ((name, pos), mut modules, _, index) = *x; + if let Some(ref mut modules) = modules { + modules.push((name, pos)); } else { let mut m: ModuleRef = Default::default(); - m.push((x.0, x.4)); - x.1 = Some(Box::new(m)); + m.push((name, pos)); + modules = Some(Box::new(m)); } - Expr::Variable(Box::new((id2, x.1, 0, x.3, pos2))) + Expr::Variable(Box::new(((id2, pos2), modules, 0, index))) } (_, pos2) => return Err(PERR::VariableExpected.into_err(pos2)), }, @@ -1226,10 +1227,11 @@ fn parse_primary<'a>( // Cache the hash key for module-qualified variables #[cfg(not(feature = "no_module"))] Expr::Variable(x) if x.1.is_some() => { - let modules = x.1.as_mut().unwrap(); + let ((name, _), modules, hash, _) = x.as_mut(); + let modules = modules.as_mut().unwrap(); // Qualifiers + variable name - x.2 = calc_fn_hash(modules.iter().map(|(v, _)| v.as_str()), &x.0, empty()); + *hash = calc_fn_hash(modules.iter().map(|(v, _)| v.as_str()), name, empty()); modules.set_index(stack.find_module(&modules.get(0).0)); } _ => (), @@ -1292,12 +1294,11 @@ fn parse_unary<'a>( let hash = calc_fn_hash(empty(), op, repeat(EMPTY_TYPE_ID()).take(2)); Ok(Expr::FnCall(Box::new(( - op.into(), + (op.into(), pos), None, hash, vec![e], None, - pos, )))) } } @@ -1310,16 +1311,17 @@ fn parse_unary<'a>( // !expr (Token::Bang, _) => { let pos = eat_token(input, Token::Bang); + let expr = vec![parse_primary(input, stack, allow_stmt_expr)?]; + let op = "!"; let hash = calc_fn_hash(empty(), op, repeat(EMPTY_TYPE_ID()).take(2)); Ok(Expr::FnCall(Box::new(( - op.into(), + (op.into(), pos), None, hash, - vec![parse_primary(input, stack, allow_stmt_expr)?], - Some(Box::new(false.into())), // NOT operator, when operating on invalid operand, defaults to false - pos, + expr, + Some(false.into()), // NOT operator, when operating on invalid operand, defaults to false )))) } // @@ -1338,11 +1340,12 @@ fn make_assignment_stmt<'a>( match &lhs { Expr::Variable(x) if x.3.is_none() => Ok(Expr::Assignment(Box::new((lhs, rhs, pos)))), Expr::Variable(x) => { - match stack[(stack.len() - x.3.unwrap().get())].1 { + let ((name, name_pos), _, _, index) = x.as_ref(); + match stack[(stack.len() - index.unwrap().get())].1 { ScopeEntryType::Normal => Ok(Expr::Assignment(Box::new((lhs, rhs, pos)))), // Constant values cannot be assigned to ScopeEntryType::Constant => { - Err(PERR::AssignmentToConstant(x.0.to_string()).into_err(x.4)) + Err(PERR::AssignmentToConstant(name.clone()).into_err(*name_pos)) } ScopeEntryType::Module => unreachable!(), } @@ -1350,11 +1353,12 @@ fn make_assignment_stmt<'a>( Expr::Index(x) | Expr::Dot(x) => match &x.0 { Expr::Variable(x) if x.3.is_none() => Ok(Expr::Assignment(Box::new((lhs, rhs, pos)))), Expr::Variable(x) => { - match stack[(stack.len() - x.3.unwrap().get())].1 { + let ((name, name_pos), _, _, index) = x.as_ref(); + match stack[(stack.len() - index.unwrap().get())].1 { ScopeEntryType::Normal => Ok(Expr::Assignment(Box::new((lhs, rhs, pos)))), // Constant values cannot be assigned to ScopeEntryType::Constant => { - Err(PERR::AssignmentToConstant(x.0.to_string()).into_err(x.4)) + Err(PERR::AssignmentToConstant(name.clone()).into_err(*name_pos)) } ScopeEntryType::Module => unreachable!(), } @@ -1403,7 +1407,7 @@ fn parse_op_assignment_stmt<'a>( // lhs op= rhs -> lhs = op(lhs, rhs) let args = vec![lhs_copy, rhs]; let hash = calc_fn_hash(empty(), op, repeat(EMPTY_TYPE_ID()).take(args.len())); - let rhs_expr = Expr::FnCall(Box::new((op.into(), None, hash, args, None, pos))); + let rhs_expr = Expr::FnCall(Box::new(((op.into(), pos), None, hash, args, None))); make_assignment_stmt(stack, lhs, rhs_expr, pos) } @@ -1423,15 +1427,13 @@ fn make_dot_expr( // lhs.id (lhs, Expr::Variable(x)) if x.1.is_none() => { let lhs = if is_index { lhs.into_property() } else { lhs }; - Expr::Dot(Box::new(( - lhs, - Expr::Property(Box::new((x.0, x.4))), - op_pos, - ))) + let rhs = Expr::Property(Box::new(x.0)); + Expr::Dot(Box::new((lhs, rhs, op_pos))) } (lhs, Expr::Property(x)) => { let lhs = if is_index { lhs.into_property() } else { lhs }; - Expr::Dot(Box::new((lhs, Expr::Property(x), op_pos))) + let rhs = Expr::Property(x); + Expr::Dot(Box::new((lhs, rhs, op_pos))) } // lhs.module::id - syntax error (_, Expr::Variable(x)) if x.1.is_some() => { @@ -1441,17 +1443,31 @@ fn make_dot_expr( return Err(PERR::PropertyExpected.into_err(x.1.unwrap().get(0).1)); } // lhs.dot_lhs.dot_rhs - (lhs, Expr::Dot(x)) => Expr::Dot(Box::new(( - lhs, - Expr::Dot(Box::new((x.0.into_property(), x.1.into_property(), x.2))), - op_pos, - ))), + (lhs, Expr::Dot(x)) => { + let (dot_lhs, dot_rhs, pos) = *x; + Expr::Dot(Box::new(( + lhs, + Expr::Dot(Box::new(( + dot_lhs.into_property(), + dot_rhs.into_property(), + pos, + ))), + op_pos, + ))) + } // lhs.idx_lhs[idx_rhs] - (lhs, Expr::Index(x)) => Expr::Dot(Box::new(( - lhs, - Expr::Index(Box::new((x.0.into_property(), x.1.into_property(), x.2))), - op_pos, - ))), + (lhs, Expr::Index(x)) => { + let (idx_lhs, idx_rhs, pos) = *x; + Expr::Dot(Box::new(( + lhs, + Expr::Index(Box::new(( + idx_lhs.into_property(), + idx_rhs.into_property(), + pos, + ))), + op_pos, + ))) + } // lhs.rhs (lhs, rhs) => Expr::Dot(Box::new((lhs, rhs.into_property(), op_pos))), }) @@ -1664,32 +1680,32 @@ fn parse_binary_op<'a>( rhs }; - let cmp_default = Some(Box::new(false.into())); + let cmp_def = Some(false.into()); let op = op_token.syntax(); let hash = calc_fn_hash(empty(), &op, repeat(EMPTY_TYPE_ID()).take(2)); let mut args = vec![current_lhs, rhs]; current_lhs = match op_token { - Token::Plus => Expr::FnCall(Box::new((op, None, hash, args, None, pos))), - Token::Minus => Expr::FnCall(Box::new((op, None, hash, args, None, pos))), - Token::Multiply => Expr::FnCall(Box::new((op, None, hash, args, None, pos))), - Token::Divide => Expr::FnCall(Box::new((op, None, hash, args, None, pos))), + Token::Plus => Expr::FnCall(Box::new(((op, pos), None, hash, args, None))), + Token::Minus => Expr::FnCall(Box::new(((op, pos), None, hash, args, None))), + Token::Multiply => Expr::FnCall(Box::new(((op, pos), None, hash, args, None))), + Token::Divide => Expr::FnCall(Box::new(((op, pos), None, hash, args, None))), - Token::LeftShift => Expr::FnCall(Box::new((op, None, hash, args, None, pos))), - Token::RightShift => Expr::FnCall(Box::new((op, None, hash, args, None, pos))), - Token::Modulo => Expr::FnCall(Box::new((op, None, hash, args, None, pos))), - Token::PowerOf => Expr::FnCall(Box::new((op, None, hash, args, None, pos))), + Token::LeftShift => Expr::FnCall(Box::new(((op, pos), None, hash, args, None))), + Token::RightShift => Expr::FnCall(Box::new(((op, pos), None, hash, args, None))), + Token::Modulo => Expr::FnCall(Box::new(((op, pos), None, hash, args, None))), + Token::PowerOf => Expr::FnCall(Box::new(((op, pos), None, hash, args, None))), // Comparison operators default to false when passed invalid operands - Token::EqualsTo => Expr::FnCall(Box::new((op, None, hash, args, cmp_default, pos))), - Token::NotEqualsTo => Expr::FnCall(Box::new((op, None, hash, args, cmp_default, pos))), - Token::LessThan => Expr::FnCall(Box::new((op, None, hash, args, cmp_default, pos))), + Token::EqualsTo => Expr::FnCall(Box::new(((op, pos), None, hash, args, cmp_def))), + Token::NotEqualsTo => Expr::FnCall(Box::new(((op, pos), None, hash, args, cmp_def))), + Token::LessThan => Expr::FnCall(Box::new(((op, pos), None, hash, args, cmp_def))), Token::LessThanEqualsTo => { - Expr::FnCall(Box::new((op, None, hash, args, cmp_default, pos))) + Expr::FnCall(Box::new(((op, pos), None, hash, args, cmp_def))) } - Token::GreaterThan => Expr::FnCall(Box::new((op, None, hash, args, cmp_default, pos))), + Token::GreaterThan => Expr::FnCall(Box::new(((op, pos), None, hash, args, cmp_def))), Token::GreaterThanEqualsTo => { - Expr::FnCall(Box::new((op, None, hash, args, cmp_default, pos))) + Expr::FnCall(Box::new(((op, pos), None, hash, args, cmp_def))) } Token::Or => { @@ -1702,9 +1718,9 @@ fn parse_binary_op<'a>( let current_lhs = args.pop().unwrap(); Expr::And(Box::new((current_lhs, rhs, pos))) } - Token::Ampersand => Expr::FnCall(Box::new((op, None, hash, args, None, pos))), - Token::Pipe => Expr::FnCall(Box::new((op, None, hash, args, None, pos))), - Token::XOr => Expr::FnCall(Box::new((op, None, hash, args, None, pos))), + Token::Ampersand => Expr::FnCall(Box::new(((op, pos), None, hash, args, None))), + Token::Pipe => Expr::FnCall(Box::new(((op, pos), None, hash, args, None))), + Token::XOr => Expr::FnCall(Box::new(((op, pos), None, hash, args, None))), Token::In => { let rhs = args.pop().unwrap(); @@ -1916,12 +1932,12 @@ fn parse_let<'a>( // let name = expr ScopeEntryType::Normal => { stack.push((name.clone(), ScopeEntryType::Normal)); - Ok(Stmt::Let(Box::new((name, Some(init_value), pos)))) + Ok(Stmt::Let(Box::new(((name, pos), Some(init_value))))) } // const name = { expr:constant } ScopeEntryType::Constant if init_value.is_constant() => { stack.push((name.clone(), ScopeEntryType::Constant)); - Ok(Stmt::Const(Box::new((name, init_value, pos)))) + Ok(Stmt::Const(Box::new(((name, pos), init_value)))) } // const name = expr - error ScopeEntryType::Constant => { @@ -1935,11 +1951,11 @@ fn parse_let<'a>( match var_type { ScopeEntryType::Normal => { stack.push((name.clone(), ScopeEntryType::Normal)); - Ok(Stmt::Let(Box::new((name, None, pos)))) + Ok(Stmt::Let(Box::new(((name, pos), None)))) } ScopeEntryType::Constant => { stack.push((name.clone(), ScopeEntryType::Constant)); - Ok(Stmt::Const(Box::new((name, Expr::Unit(pos), pos)))) + Ok(Stmt::Const(Box::new(((name, pos), Expr::Unit(pos))))) } // Variable cannot be a module ScopeEntryType::Module => unreachable!(), @@ -1978,7 +1994,7 @@ fn parse_import<'a>( }; stack.push((name.clone(), ScopeEntryType::Module)); - Ok(Stmt::Import(Box::new((expr, name, pos)))) + Ok(Stmt::Import(Box::new((expr, (name, pos))))) } /// Parse an export statement. @@ -2005,7 +2021,7 @@ fn parse_export<'a>(input: &mut Peekable>) -> Result { @@ -2026,14 +2042,14 @@ fn parse_export<'a>(input: &mut Peekable>) -> Result( match input.peek().unwrap() { // `return`/`throw` at - (Token::EOF, pos) => Ok(Stmt::ReturnWithVal(Box::new((None, return_type, *pos)))), + (Token::EOF, pos) => Ok(Stmt::ReturnWithVal(Box::new(((return_type, *pos), None)))), // `return;` or `throw;` (Token::SemiColon, _) => { - Ok(Stmt::ReturnWithVal(Box::new((None, return_type, pos)))) + Ok(Stmt::ReturnWithVal(Box::new(((return_type, pos), None)))) } // `return` or `throw` with expression (_, _) => { @@ -2176,9 +2192,8 @@ fn parse_stmt<'a>( let pos = expr.position(); Ok(Stmt::ReturnWithVal(Box::new(( + (return_type, pos), Some(expr), - return_type, - pos, )))) } } @@ -2448,13 +2463,14 @@ pub fn map_dynamic_to_expr(value: Dynamic, pos: Position) -> Option { Union::Map(map) => { let items: Vec<_> = map .into_iter() - .map(|(k, v)| (k, map_dynamic_to_expr(v, pos), pos)) + .map(|(k, v)| ((k, pos), map_dynamic_to_expr(v, pos))) .collect(); - if items.iter().all(|(_, expr, _)| expr.is_some()) { + + if items.iter().all(|(_, expr)| expr.is_some()) { Some(Expr::Map(Box::new(( items .into_iter() - .map(|(k, expr, pos)| (k, expr.unwrap(), pos)) + .map(|((k, pos), expr)| ((k, pos), expr.unwrap())) .collect(), pos, )))) diff --git a/tests/modules.rs b/tests/modules.rs index 1fc4d895..52f6818e 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -18,12 +18,7 @@ fn test_module_sub_module() -> Result<(), Box> { let mut sub_module2 = Module::new(); sub_module2.set_var("answer", 41 as INT); - let hash_inc = sub_module2.set_fn_1("inc", |x: INT| Ok(x + 1), false); - let hash_hidden = sub_module2.set_fn_0( - "hidden", - || Err("shouldn't see me!".into()) as Result<(), Box>, - true, - ); + let hash_inc = sub_module2.set_fn_1("inc", |x: INT| Ok(x + 1)); sub_module.set_sub_module("universe", sub_module2); module.set_sub_module("life", sub_module); @@ -36,7 +31,6 @@ fn test_module_sub_module() -> Result<(), Box> { assert!(m2.contains_var("answer")); assert!(m2.contains_fn(hash_inc)); - assert!(m2.contains_fn(hash_hidden)); assert_eq!(m2.get_var_value::("answer").unwrap(), 41); @@ -59,11 +53,6 @@ fn test_module_sub_module() -> Result<(), Box> { )?, 42 ); - assert!(matches!( - *engine.eval_expression_with_scope::<()>(&mut scope, "question::life::universe::hidden()") - .expect_err("should error"), - EvalAltResult::ErrorFunctionNotFound(fn_name, _) if fn_name == "hidden" - )); Ok(()) } From cd570558c32e9b67e298d524a542a3e4ef9174d8 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 21 May 2020 09:31:31 +0800 Subject: [PATCH 04/53] Remove count_args macro. --- src/fn_register.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/fn_register.rs b/src/fn_register.rs index 4b98f184..f33c5855 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -190,12 +190,6 @@ impl RegisterPlugin for Engine { } } -/// This macro counts the number of arguments via recursion. -macro_rules! count_args { - () => { 0_usize }; - ( $head:ident $($tail:ident)* ) => { 1_usize + count_args!($($tail)*) }; -} - /// This macro creates a closure wrapping a registered function. macro_rules! make_func { ($fn:ident : $map:expr ; $($par:ident => $convert:expr),*) => { From cec6748ac6271cdb6469c7f93765cd31bdd2d067 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 1 Jun 2020 10:58:14 +0800 Subject: [PATCH 05/53] Remove no_module gates to prepare for plugins. --- README.md | 2 +- src/any.rs | 14 +------- src/engine.rs | 89 +++++++++++++++++++++--------------------------- src/module.rs | 22 +++--------- src/parser.rs | 85 +++++++++++++++++---------------------------- src/scope.rs | 20 +++++++++-- src/token.rs | 8 ++--- tests/modules.rs | 1 + 8 files changed, 99 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index f894ea7b..bd0cc029 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ Optional features | `no_index` | Disable [arrays] and indexing features. | | `no_object` | Disable support for custom types and [object maps]. | | `no_function` | Disable script-defined functions. | -| `no_module` | Disable loading modules. | +| `no_module` | Disable loading external modules. | | `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | By default, Rhai includes all the standard functionalities in a small, tight package. diff --git a/src/any.rs b/src/any.rs index f685c303..0b6d81d2 100644 --- a/src/any.rs +++ b/src/any.rs @@ -1,11 +1,9 @@ //! Helper module which defines the `Any` trait to to allow dynamic value handling. +use crate::module::Module; use crate::parser::{ImmutableString, INT}; use crate::r#unsafe::{unsafe_cast_box, unsafe_try_cast}; -#[cfg(not(feature = "no_module"))] -use crate::module::Module; - #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; @@ -160,7 +158,6 @@ pub enum Union { Array(Box), #[cfg(not(feature = "no_object"))] Map(Box), - #[cfg(not(feature = "no_module"))] Module(Box), Variant(Box>), } @@ -198,7 +195,6 @@ impl Dynamic { Union::Array(_) => TypeId::of::(), #[cfg(not(feature = "no_object"))] Union::Map(_) => TypeId::of::(), - #[cfg(not(feature = "no_module"))] Union::Module(_) => TypeId::of::(), Union::Variant(value) => (***value).type_id(), } @@ -218,7 +214,6 @@ impl Dynamic { Union::Array(_) => "array", #[cfg(not(feature = "no_object"))] Union::Map(_) => "map", - #[cfg(not(feature = "no_module"))] Union::Module(_) => "sub-scope", #[cfg(not(feature = "no_std"))] @@ -242,7 +237,6 @@ impl fmt::Display for Dynamic { Union::Array(value) => write!(f, "{:?}", value), #[cfg(not(feature = "no_object"))] Union::Map(value) => write!(f, "#{:?}", value), - #[cfg(not(feature = "no_module"))] Union::Module(value) => write!(f, "{:?}", value), #[cfg(not(feature = "no_std"))] @@ -266,7 +260,6 @@ impl fmt::Debug for Dynamic { Union::Array(value) => write!(f, "{:?}", value), #[cfg(not(feature = "no_object"))] Union::Map(value) => write!(f, "#{:?}", value), - #[cfg(not(feature = "no_module"))] Union::Module(value) => write!(f, "{:?}", value), #[cfg(not(feature = "no_std"))] @@ -290,7 +283,6 @@ impl Clone for Dynamic { Union::Array(ref value) => Self(Union::Array(value.clone())), #[cfg(not(feature = "no_object"))] Union::Map(ref value) => Self(Union::Map(value.clone())), - #[cfg(not(feature = "no_module"))] Union::Module(ref value) => Self(Union::Module(value.clone())), Union::Variant(ref value) => (***value).clone_into_dynamic(), } @@ -426,7 +418,6 @@ impl Dynamic { Union::Array(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), #[cfg(not(feature = "no_object"))] Union::Map(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), - #[cfg(not(feature = "no_module"))] Union::Module(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), Union::Variant(value) => (*value).as_box_any().downcast().map(|x| *x).ok(), } @@ -470,7 +461,6 @@ impl Dynamic { Union::Array(value) => *unsafe_cast_box::<_, T>(value).unwrap(), #[cfg(not(feature = "no_object"))] Union::Map(value) => *unsafe_cast_box::<_, T>(value).unwrap(), - #[cfg(not(feature = "no_module"))] Union::Module(value) => *unsafe_cast_box::<_, T>(value).unwrap(), Union::Variant(value) => (*value).as_box_any().downcast().map(|x| *x).unwrap(), } @@ -498,7 +488,6 @@ impl Dynamic { Union::Array(value) => (value.as_ref() as &dyn Any).downcast_ref::(), #[cfg(not(feature = "no_object"))] Union::Map(value) => (value.as_ref() as &dyn Any).downcast_ref::(), - #[cfg(not(feature = "no_module"))] Union::Module(value) => (value.as_ref() as &dyn Any).downcast_ref::(), Union::Variant(value) => value.as_ref().as_ref().as_any().downcast_ref::(), } @@ -524,7 +513,6 @@ impl Dynamic { Union::Array(value) => (value.as_mut() as &mut dyn Any).downcast_mut::(), #[cfg(not(feature = "no_object"))] Union::Map(value) => (value.as_mut() as &mut dyn Any).downcast_mut::(), - #[cfg(not(feature = "no_module"))] Union::Module(value) => (value.as_mut() as &mut dyn Any).downcast_mut::(), Union::Variant(value) => value.as_mut().as_mut_any().downcast_mut::(), } diff --git a/src/engine.rs b/src/engine.rs index 03521f4f..453ee184 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -4,7 +4,7 @@ use crate::any::{Dynamic, Union}; use crate::calc_fn_hash; use crate::error::ParseErrorType; use crate::fn_native::{CallableFunction, FnCallArgs, Shared}; -use crate::module::Module; +use crate::module::{resolvers, Module, ModuleResolver}; use crate::optimize::OptimizationLevel; use crate::packages::{CorePackage, Package, PackageLibrary, PackagesCollection, StandardPackage}; use crate::parser::{Expr, FnAccess, FnDef, ImmutableString, ReturnType, Stmt, AST, INT}; @@ -17,9 +17,6 @@ use crate::utils::{StaticVec, StraightHasherBuilder}; #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; -#[cfg(not(feature = "no_module"))] -use crate::module::{resolvers, ModuleResolver}; - use crate::stdlib::{ any::TypeId, boxed::Box, @@ -297,7 +294,6 @@ pub struct Engine { pub(crate) packages: PackagesCollection, /// A module resolution service. - #[cfg(not(feature = "no_module"))] pub(crate) module_resolver: Option>, /// A hashmap mapping type names to pretty-print names. @@ -350,8 +346,7 @@ impl Default for Engine { #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_std"))] module_resolver: Some(Box::new(resolvers::FileModuleResolver::new())), - #[cfg(not(feature = "no_module"))] - #[cfg(feature = "no_std")] + #[cfg(any(feature = "no_module", feature = "no_std"))] module_resolver: None, type_names: HashMap::new(), @@ -442,31 +437,28 @@ fn search_scope<'s, 'a>( _ => unreachable!(), }; - #[cfg(not(feature = "no_module"))] - { - if let Some(modules) = modules.as_ref() { - let module = if let Some(index) = modules.index() { - scope - .get_mut(scope.len() - index.get()) - .0 - .downcast_mut::() - .unwrap() - } else { - let (id, root_pos) = modules.get(0); + if let Some(modules) = modules.as_ref() { + let module = if let Some(index) = modules.index() { + scope + .get_mut(scope.len() - index.get()) + .0 + .downcast_mut::() + .unwrap() + } else { + let (id, root_pos) = modules.get(0); - scope.find_module(id).ok_or_else(|| { - Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)) - })? - }; + scope + .find_module_internal(id) + .ok_or_else(|| Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)))? + }; - return Ok(( - module.get_qualified_var_mut(name, *hash_var, *pos)?, - name, - // Module variables are constant - ScopeEntryType::Constant, - *pos, - )); - } + return Ok(( + module.get_qualified_var_mut(name, *hash_var, *pos)?, + name, + // Module variables are constant + ScopeEntryType::Constant, + *pos, + )); } let index = if state.always_search { None } else { *index }; @@ -496,8 +488,6 @@ impl Engine { Self { packages: Default::default(), global_module: Default::default(), - - #[cfg(not(feature = "no_module"))] module_resolver: None, type_names: HashMap::new(), @@ -1609,7 +1599,6 @@ impl Engine { } // Module-qualified function call - #[cfg(not(feature = "no_module"))] Expr::FnCall(x) if x.1.is_some() => { let ((name, _, pos), modules, hash_fn_def, args_expr, def_val) = x.as_ref(); let modules = modules.as_ref().unwrap(); @@ -1630,7 +1619,7 @@ impl Engine { .downcast_mut::() .unwrap() } else { - scope.find_module(id).ok_or_else(|| { + scope.find_module_internal(id).ok_or_else(|| { Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)) })? }; @@ -1919,21 +1908,18 @@ impl Engine { // Import statement Stmt::Import(x) => { - #[cfg(feature = "no_module")] - unreachable!(); + let (expr, (name, pos)) = x.as_ref(); - #[cfg(not(feature = "no_module"))] + // Guard against too many modules + if state.modules >= self.max_modules { + return Err(Box::new(EvalAltResult::ErrorTooManyModules(*pos))); + } + + if let Some(path) = self + .eval_expr(scope, state, lib, &expr, level)? + .try_cast::() { - let (expr, (name, pos)) = x.as_ref(); - - // Guard against too many modules - if state.modules >= self.max_modules { - return Err(Box::new(EvalAltResult::ErrorTooManyModules(*pos))); - } - - if let Some(path) = self - .eval_expr(scope, state, lib, &expr, level)? - .try_cast::() + #[cfg(not(feature = "no_module"))] { if let Some(resolver) = &self.module_resolver { // Use an empty scope to create a module @@ -1941,7 +1927,7 @@ impl Engine { resolver.resolve(self, Scope::new(), &path, expr.position())?; let mod_name = unsafe_cast_var_name_to_lifetime(name, &state); - scope.push_module(mod_name, module); + scope.push_module_internal(mod_name, module); state.modules += 1; @@ -1952,9 +1938,12 @@ impl Engine { expr.position(), ))) } - } else { - Err(Box::new(EvalAltResult::ErrorImportExpr(expr.position()))) } + + #[cfg(feature = "no_module")] + Ok(Default::default()) + } else { + Err(Box::new(EvalAltResult::ErrorImportExpr(expr.position()))) } } diff --git a/src/module.rs b/src/module.rs index 9e16e276..cd567a57 100644 --- a/src/module.rs +++ b/src/module.rs @@ -3,7 +3,7 @@ use crate::any::{Dynamic, Variant}; use crate::calc_fn_hash; use crate::engine::{make_getter, make_setter, Engine, FunctionsLib, FUNC_INDEXER}; -use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn}; +use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync}; use crate::parser::{ FnAccess, FnAccess::{Private, Public}, @@ -945,23 +945,7 @@ impl ModuleRef { } /// Trait that encapsulates a module resolution service. -#[cfg(not(feature = "no_module"))] -#[cfg(not(feature = "sync"))] -pub trait ModuleResolver { - /// Resolve a module based on a path string. - fn resolve( - &self, - engine: &Engine, - scope: Scope, - path: &str, - pos: Position, - ) -> Result>; -} - -/// Trait that encapsulates a module resolution service. -#[cfg(not(feature = "no_module"))] -#[cfg(feature = "sync")] -pub trait ModuleResolver: Send + Sync { +pub trait ModuleResolver: SendSync { /// Resolve a module based on a path string. fn resolve( &self, @@ -979,6 +963,8 @@ pub mod resolvers { pub use super::file::FileModuleResolver; pub use super::stat::StaticModuleResolver; } +#[cfg(feature = "no_module")] +pub mod resolvers {} /// Script file-based module resolver. #[cfg(not(feature = "no_module"))] diff --git a/src/parser.rs b/src/parser.rs index 2a20422e..8005acbc 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4,18 +4,12 @@ use crate::any::{Dynamic, Union}; use crate::calc_fn_hash; use crate::engine::{make_getter, make_setter, Engine, FunctionsLib}; use crate::error::{LexError, ParseError, ParseErrorType}; +use crate::module::ModuleRef; use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::scope::{EntryType as ScopeEntryType, Scope}; use crate::token::{Position, Token, TokenIterator}; use crate::utils::{StaticVec, StraightHasherBuilder}; -#[cfg(not(feature = "no_module"))] -use crate::module::ModuleRef; - -#[cfg(feature = "no_module")] -#[derive(Debug, Eq, PartialEq, Clone, Hash, Copy, Default)] -pub struct ModuleRef; - use crate::stdlib::{ borrow::Cow, boxed::Box, @@ -644,7 +638,6 @@ impl Expr { Self::Variable(_) => match token { Token::LeftBracket | Token::LeftParen => true, - #[cfg(not(feature = "no_module"))] Token::DoubleColon => true, _ => false, }, @@ -761,27 +754,21 @@ fn parse_call_expr<'a>( Token::RightParen => { eat_token(input, Token::RightParen); - #[cfg(not(feature = "no_module"))] - let hash_fn_def = { - if let Some(modules) = modules.as_mut() { - modules.set_index(state.find_module(&modules.get(0).0)); + let hash_fn_def = if let Some(modules) = modules.as_mut() { + modules.set_index(state.find_module(&modules.get(0).0)); - // Rust functions are indexed in two steps: - // 1) Calculate a hash in a similar manner to script-defined functions, - // i.e. qualifiers + function name + number of arguments. - // 2) Calculate a second hash with no qualifiers, empty function name, - // zero number of arguments, and the actual list of argument `TypeId`'s. - // 3) The final hash is the XOR of the two hashes. - let qualifiers = modules.iter().map(|(m, _)| m.as_str()); - calc_fn_hash(qualifiers, &id, 0, empty()) - } else { - // Qualifiers (none) + function name + no parameters. - calc_fn_hash(empty(), &id, 0, empty()) - } + // Rust functions are indexed in two steps: + // 1) Calculate a hash in a similar manner to script-defined functions, + // i.e. qualifiers + function name + number of arguments. + // 2) Calculate a second hash with no qualifiers, empty function name, + // zero number of arguments, and the actual list of argument `TypeId`'s. + // 3) The final hash is the XOR of the two hashes. + let qualifiers = modules.iter().map(|(m, _)| m.as_str()); + calc_fn_hash(qualifiers, &id, 0, empty()) + } else { + // Qualifiers (none) + function name + no parameters. + calc_fn_hash(empty(), &id, 0, empty()) }; - // Qualifiers (none) + function name + no parameters. - #[cfg(feature = "no_module")] - let hash_fn_def = calc_fn_hash(empty(), &id, 0, empty()); return Ok(Expr::FnCall(Box::new(( (id.into(), false, begin), @@ -803,27 +790,21 @@ fn parse_call_expr<'a>( (Token::RightParen, _) => { eat_token(input, Token::RightParen); - #[cfg(not(feature = "no_module"))] - let hash_fn_def = { - if let Some(modules) = modules.as_mut() { - modules.set_index(state.find_module(&modules.get(0).0)); + let hash_fn_def = if let Some(modules) = modules.as_mut() { + modules.set_index(state.find_module(&modules.get(0).0)); - // Rust functions are indexed in two steps: - // 1) Calculate a hash in a similar manner to script-defined functions, - // i.e. qualifiers + function name + number of arguments. - // 2) Calculate a second hash with no qualifiers, empty function name, - // zero number of arguments, and the actual list of argument `TypeId`'s. - // 3) The final hash is the XOR of the two hashes. - let qualifiers = modules.iter().map(|(m, _)| m.as_str()); - calc_fn_hash(qualifiers, &id, args.len(), empty()) - } else { - // Qualifiers (none) + function name + number of arguments. - calc_fn_hash(empty(), &id, args.len(), empty()) - } + // Rust functions are indexed in two steps: + // 1) Calculate a hash in a similar manner to script-defined functions, + // i.e. qualifiers + function name + number of arguments. + // 2) Calculate a second hash with no qualifiers, empty function name, + // zero number of arguments, and the actual list of argument `TypeId`'s. + // 3) The final hash is the XOR of the two hashes. + let qualifiers = modules.iter().map(|(m, _)| m.as_str()); + calc_fn_hash(qualifiers, &id, args.len(), empty()) + } else { + // Qualifiers (none) + function name + number of arguments. + calc_fn_hash(empty(), &id, args.len(), empty()) }; - // Qualifiers (none) + function name + number of arguments. - #[cfg(feature = "no_module")] - let hash_fn_def = calc_fn_hash(empty(), &id, args.len(), empty()); return Ok(Expr::FnCall(Box::new(( (id.into(), false, begin), @@ -1265,7 +1246,6 @@ fn parse_primary<'a>( } (Expr::Property(_), _) => unreachable!(), // module access - #[cfg(not(feature = "no_module"))] (Expr::Variable(x), Token::DoubleColon) => match input.next().unwrap() { (Token::Identifier(id2), pos2) => { let ((name, pos), mut modules, _, index) = *x; @@ -1293,7 +1273,6 @@ fn parse_primary<'a>( match &mut root_expr { // Cache the hash key for module-qualified variables - #[cfg(not(feature = "no_module"))] Expr::Variable(x) if x.1.is_some() => { let ((name, _), modules, hash, _) = x.as_mut(); let modules = modules.as_mut().unwrap(); @@ -1520,9 +1499,6 @@ fn make_dot_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { - #[cfg(feature = "no_module")] - unreachable!(); - #[cfg(not(feature = "no_module"))] return Err(PERR::PropertyExpected.into_err(x.1.unwrap().get(0).1)); } // lhs.prop @@ -2113,6 +2089,7 @@ fn parse_import<'a>( } /// Parse an export statement. +#[cfg(not(feature = "no_module"))] fn parse_export<'a>( input: &mut Peekable>, state: &mut ParseState, @@ -2294,7 +2271,9 @@ fn parse_stmt<'a>( Token::LeftBrace => parse_block(input, state, breakable, level + 1, allow_stmt_expr), // fn ... + #[cfg(not(feature = "no_function"))] Token::Fn if !is_global => Err(PERR::WrongFnDefinition.into_err(*pos)), + #[cfg(not(feature = "no_function"))] Token::Fn => unreachable!(), Token::If => parse_if(input, state, breakable, level + 1, allow_stmt_expr), @@ -2343,8 +2322,6 @@ fn parse_stmt<'a>( Token::Let => parse_let(input, state, Normal, level + 1, allow_stmt_expr), Token::Const => parse_let(input, state, Constant, level + 1, allow_stmt_expr), - - #[cfg(not(feature = "no_module"))] Token::Import => parse_import(input, state, level + 1, allow_stmt_expr), #[cfg(not(feature = "no_module"))] @@ -2358,6 +2335,7 @@ fn parse_stmt<'a>( } /// Parse a function definition. +#[cfg(not(feature = "no_function"))] fn parse_fn<'a>( input: &mut Peekable>, state: &mut ParseState, @@ -2499,6 +2477,7 @@ fn parse_global_level<'a>( }; match input.peek().unwrap() { + #[cfg(not(feature = "no_function"))] (Token::Fn, _) => { let mut state = ParseState::new(max_expr_depth.1); let func = parse_fn(input, &mut state, access, 0, true)?; diff --git a/src/scope.rs b/src/scope.rs index 1b9c8b1d..50a5694f 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,12 +1,10 @@ //! Module that defines the `Scope` type representing a function call-stack scope. use crate::any::{Dynamic, Union, Variant}; +use crate::module::Module; use crate::parser::{map_dynamic_to_expr, Expr}; use crate::token::Position; -#[cfg(not(feature = "no_module"))] -use crate::module::Module; - use crate::stdlib::{borrow::Cow, boxed::Box, iter, string::String, vec::Vec}; /// Type of an entry in the Scope. @@ -178,6 +176,17 @@ impl<'a> Scope<'a> { /// Modules are used for accessing member variables, functions and plugins under a namespace. #[cfg(not(feature = "no_module"))] pub fn push_module>>(&mut self, name: K, mut value: Module) { + self.push_module_internal(name, value); + } + + /// Add (push) a new module to the Scope. + /// + /// Modules are used for accessing member variables, functions and plugins under a namespace. + pub(crate) fn push_module_internal>>( + &mut self, + name: K, + mut value: Module, + ) { value.index_all_sub_modules(); self.push_dynamic_value( @@ -350,6 +359,11 @@ impl<'a> Scope<'a> { /// Find a module in the Scope, starting from the last entry. #[cfg(not(feature = "no_module"))] pub fn find_module(&mut self, name: &str) -> Option<&mut Module> { + self.find_module_internal(name) + } + + /// Find a module in the Scope, starting from the last entry. + pub(crate) fn find_module_internal(&mut self, name: &str) -> Option<&mut Module> { let index = self.get_module_index(name)?; self.get_mut(index).0.downcast_mut::() } diff --git a/src/token.rs b/src/token.rs index dda005a9..9e6613b0 100644 --- a/src/token.rs +++ b/src/token.rs @@ -181,6 +181,7 @@ pub enum Token { XOr, Ampersand, And, + #[cfg(not(feature = "no_function"))] Fn, Continue, Break, @@ -199,6 +200,7 @@ pub enum Token { PowerOfAssign, Private, Import, + #[cfg(not(feature = "no_module"))] Export, As, LexError(Box), @@ -260,6 +262,7 @@ impl Token { Or => "||", Ampersand => "&", And => "&&", + #[cfg(not(feature = "no_function"))] Fn => "fn", Continue => "continue", Break => "break", @@ -283,6 +286,7 @@ impl Token { PowerOfAssign => "~=", Private => "private", Import => "import", + #[cfg(not(feature = "no_module"))] Export => "export", As => "as", EOF => "{EOF}", @@ -754,12 +758,9 @@ impl<'a> TokenIterator<'a> { "for" => Token::For, "in" => Token::In, "private" => Token::Private, - - #[cfg(not(feature = "no_module"))] "import" => Token::Import, #[cfg(not(feature = "no_module"))] "export" => Token::Export, - #[cfg(not(feature = "no_module"))] "as" => Token::As, #[cfg(not(feature = "no_function"))] @@ -916,7 +917,6 @@ impl<'a> TokenIterator<'a> { } ('=', _) => return Some((Token::Equals, pos)), - #[cfg(not(feature = "no_module"))] (':', ':') => { self.eat_next(); return Some((Token::DoubleColon, pos)); diff --git a/tests/modules.rs b/tests/modules.rs index 61273f53..a063e1bd 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -195,6 +195,7 @@ fn test_module_from_ast() -> Result<(), Box> { let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?; let mut scope = Scope::new(); + scope.push_module("testing", module); assert_eq!( From 6d190096fdf081061c2114acb39ab46d65eee888 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 1 Jun 2020 13:03:01 +0800 Subject: [PATCH 06/53] Fix bug with bracket postfix when no_index. --- src/parser.rs | 19 ++++++++++++++----- tests/string.rs | 17 ++++++++++++++++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 8005acbc..c8f71d7c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -622,28 +622,33 @@ impl Expr { | Self::Or(_) | Self::True(_) | Self::False(_) - | Self::Unit(_) => false, + | Self::Unit(_) + | Self::Assignment(_) => false, Self::StringConstant(_) | Self::Stmt(_) | Self::FnCall(_) - | Self::Assignment(_) | Self::Dot(_) | Self::Index(_) | Self::Array(_) | Self::Map(_) => match token { + #[cfg(not(feature = "no_index"))] Token::LeftBracket => true, _ => false, }, Self::Variable(_) => match token { - Token::LeftBracket | Token::LeftParen => true, + #[cfg(not(feature = "no_index"))] + Token::LeftBracket => true, + Token::LeftParen => true, Token::DoubleColon => true, _ => false, }, Self::Property(_) => match token { - Token::LeftBracket | Token::LeftParen => true, + #[cfg(not(feature = "no_index"))] + Token::LeftBracket => true, + Token::LeftParen => true, _ => false, }, } @@ -1267,7 +1272,11 @@ fn parse_primary<'a>( parse_index_chain(input, state, expr, token_pos, level + 1, allow_stmt_expr)? } // Unknown postfix operator - (expr, token) => panic!("unknown postfix operator {:?} for {:?}", token, expr), + (expr, token) => panic!( + "unknown postfix operator '{}' for {:?}", + token.syntax(), + expr + ), } } diff --git a/tests/string.rs b/tests/string.rs index b868f050..995bd76e 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -23,9 +23,24 @@ fn test_string() -> Result<(), Box> { assert_eq!(engine.eval::(r#""foo" + 123"#)?, "foo123"); #[cfg(not(feature = "no_object"))] - assert_eq!(engine.eval::("(42).to_string()")?, "42"); + assert_eq!(engine.eval::("to_string(42)")?, "42"); + + #[cfg(not(feature = "no_index"))] + assert_eq!(engine.eval::(r#"let y = "hello"; y[1]"#)?, 'e'); #[cfg(not(feature = "no_object"))] + assert_eq!(engine.eval::(r#"let y = "hello"; y.len"#)?, 5); + + #[cfg(not(feature = "no_object"))] + assert_eq!( + engine.eval::(r#"let y = "hello"; y.clear(); y.len"#)?, + 0 + ); + + assert_eq!(engine.eval::(r#"let y = "hello"; len(y)"#)?, 5); + + #[cfg(not(feature = "no_object"))] + #[cfg(not(feature = "no_index"))] assert_eq!(engine.eval::(r#"let y = "hello"; y[y.len-1]"#)?, 'o'); #[cfg(not(feature = "no_float"))] From 3f9d0895defb7e739c3f96bdb10d185a517a27d1 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 1 Jun 2020 13:26:02 +0800 Subject: [PATCH 07/53] Bump version. --- Cargo.toml | 2 +- README.md | 8 ++++---- RELEASES.md | 6 +++++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 23f4aa90..210b7488 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rhai" -version = "0.15.0" +version = "0.16.0" edition = "2018" authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung"] description = "Embedded scripting for Rust" diff --git a/README.md b/README.md index bd0cc029..aaf394fc 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Features to do checked arithmetic operations); for [`no-std`](#optional-features) builds, a number of additional dependencies are pulled in to provide for functionalities that used to be in `std`. -**Note:** Currently, the version is `0.15.0`, so the language and API's may change before they stabilize. +**Note:** Currently, the version is `0.16.0`, so the language and API's may change before they stabilize. What Rhai doesn't do -------------------- @@ -71,7 +71,7 @@ Install the Rhai crate on [`crates.io`](https::/crates.io/crates/rhai/) by addin ```toml [dependencies] -rhai = "0.15.0" +rhai = "0.16.0" ``` Use the latest released crate version on [`crates.io`](https::/crates.io/crates/rhai/): @@ -427,7 +427,7 @@ are supported. | `+`, | `+=` | `INT`, `FLOAT` (if not [`no_float`]), `ImmutableString` | | `-`, `*`, `/`, `%`, `~`, | `-=`, `*=`, `/=`, `%=`, `~=` | `INT`, `FLOAT` (if not [`no_float`]) | | `<<`, `>>`, `^`, | `<<=`, `>>=`, `^=` | `INT` | -| `&`, `\|`, | `&=`, `|=` | `INT`, `bool` | +| `&`, `\|`, | `&=`, `\|=` | `INT`, `bool` | | `&&`, `\|\|` | | `bool` | | `==`, `!=` | | `INT`, `FLOAT` (if not [`no_float`]), `bool`, `char`, `()`, `ImmutableString` | | `>`, `>=`, `<`, `<=` | | `INT`, `FLOAT` (if not [`no_float`]), `char`, `()`, `ImmutableString` | @@ -496,7 +496,7 @@ In these cases, use the `compile_expression` and `eval_expression` methods or th let result = engine.eval_expression::("2 + (10 + 10) * 2")?; ``` -When evaluation _expressions_, no full-blown statement (e.g. `if`, `while`, `for`) - not even variable assignments - +When evaluating _expressions_, no full-blown statement (e.g. `if`, `while`, `for`) - not even variable assignments - is supported and will be considered parse errors when encountered. ```rust diff --git a/RELEASES.md b/RELEASES.md index 1208c720..2bb7b783 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,7 +1,11 @@ Rhai Release Notes ================== -Version 0.14.2 +Version 0.16.0 +============== + + +Version 0.15.0 ============== Regression fix From b8b12055b9aaa1f693fc1d74dff55bed3afeb581 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 1 Jun 2020 13:26:20 +0800 Subject: [PATCH 08/53] Disable if-expression when parsing expressions. --- src/parser.rs | 186 ++++++++++++++++++++++++++----------------- tests/expressions.rs | 7 +- 2 files changed, 117 insertions(+), 76 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index c8f71d7c..5ff5e692 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -700,7 +700,8 @@ fn parse_paren_expr<'a>( state: &mut ParseState, pos: Position, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { if level > state.max_expr_depth { return Err(PERR::ExprTooDeep.into_err(pos)); @@ -710,7 +711,7 @@ fn parse_paren_expr<'a>( return Ok(Expr::Unit(pos)); } - let expr = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let expr = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; match input.next().unwrap() { // ( xxx ) @@ -734,7 +735,8 @@ fn parse_call_expr<'a>( mut modules: Option>, begin: Position, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { let (token, pos) = input.peek().unwrap(); @@ -788,7 +790,7 @@ fn parse_call_expr<'a>( } loop { - args.push(parse_expr(input, state, level + 1, allow_stmt_expr)?); + args.push(parse_expr(input, state, level + 1, if_expr, stmt_expr)?); match input.peek().unwrap() { // id(...args) @@ -855,13 +857,14 @@ fn parse_index_chain<'a>( lhs: Expr, pos: Position, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { if level > state.max_expr_depth { return Err(PERR::ExprTooDeep.into_err(pos)); } - let idx_expr = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let idx_expr = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; // Check type of indexing - must be integer or string match &idx_expr { @@ -1008,7 +1011,8 @@ fn parse_index_chain<'a>( idx_expr, idx_pos, level + 1, - allow_stmt_expr, + if_expr, + stmt_expr, )?; // Indexing binds to right Ok(Expr::Index(Box::new((lhs, idx_expr, pos)))) @@ -1042,7 +1046,8 @@ fn parse_array_literal<'a>( state: &mut ParseState, pos: Position, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { if level > state.max_expr_depth { return Err(PERR::ExprTooDeep.into_err(pos)); @@ -1052,7 +1057,8 @@ fn parse_array_literal<'a>( if !match_token(input, Token::RightBracket)? { while !input.peek().unwrap().0.is_eof() { - arr.push(parse_expr(input, state, level + 1, allow_stmt_expr)?); + let expr = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; + arr.push(expr); match input.peek().unwrap() { (Token::Comma, _) => eat_token(input, Token::Comma), @@ -1090,7 +1096,8 @@ fn parse_map_literal<'a>( state: &mut ParseState, pos: Position, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { if level > state.max_expr_depth { return Err(PERR::ExprTooDeep.into_err(pos)); @@ -1140,7 +1147,7 @@ fn parse_map_literal<'a>( } }; - let expr = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let expr = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; map.push(((name, pos), expr)); @@ -1191,7 +1198,8 @@ fn parse_primary<'a>( input: &mut Peekable>, state: &mut ParseState, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { let (token, pos) = input.peek().unwrap(); let pos = *pos; @@ -1202,8 +1210,8 @@ fn parse_primary<'a>( let (token, _) = match token { // { - block statement as expression - Token::LeftBrace if allow_stmt_expr => { - return parse_block(input, state, false, level + 1, allow_stmt_expr) + Token::LeftBrace if stmt_expr => { + return parse_block(input, state, false, level + 1, if_expr, stmt_expr) .map(|block| Expr::Stmt(Box::new((block, pos)))); } Token::EOF => return Err(PERR::UnexpectedEOF.into_err(pos)), @@ -1220,11 +1228,13 @@ fn parse_primary<'a>( let index = state.find(&s); Expr::Variable(Box::new(((s, pos), None, 0, index))) } - Token::LeftParen => parse_paren_expr(input, state, pos, level + 1, allow_stmt_expr)?, + Token::LeftParen => parse_paren_expr(input, state, pos, level + 1, if_expr, stmt_expr)?, #[cfg(not(feature = "no_index"))] - Token::LeftBracket => parse_array_literal(input, state, pos, level + 1, allow_stmt_expr)?, + Token::LeftBracket => { + parse_array_literal(input, state, pos, level + 1, if_expr, stmt_expr)? + } #[cfg(not(feature = "no_object"))] - Token::MapStart => parse_map_literal(input, state, pos, level + 1, allow_stmt_expr)?, + Token::MapStart => parse_map_literal(input, state, pos, level + 1, if_expr, stmt_expr)?, Token::True => Expr::True(pos), Token::False => Expr::False(pos), Token::LexError(err) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), @@ -1247,7 +1257,16 @@ fn parse_primary<'a>( // Function call (Expr::Variable(x), Token::LeftParen) => { let ((name, pos), modules, _, _) = *x; - parse_call_expr(input, state, name, modules, pos, level + 1, allow_stmt_expr)? + parse_call_expr( + input, + state, + name, + modules, + pos, + level + 1, + if_expr, + stmt_expr, + )? } (Expr::Property(_), _) => unreachable!(), // module access @@ -1269,7 +1288,7 @@ fn parse_primary<'a>( // Indexing #[cfg(not(feature = "no_index"))] (expr, Token::LeftBracket) => { - parse_index_chain(input, state, expr, token_pos, level + 1, allow_stmt_expr)? + parse_index_chain(input, state, expr, token_pos, level + 1, if_expr, stmt_expr)? } // Unknown postfix operator (expr, token) => panic!( @@ -1301,7 +1320,8 @@ fn parse_unary<'a>( input: &mut Peekable>, state: &mut ParseState, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { let (token, pos) = input.peek().unwrap(); let pos = *pos; @@ -1312,15 +1332,15 @@ fn parse_unary<'a>( match token { // If statement is allowed to act as expressions - Token::If => Ok(Expr::Stmt(Box::new(( - parse_if(input, state, false, level + 1, allow_stmt_expr)?, + Token::If if if_expr => Ok(Expr::Stmt(Box::new(( + parse_if(input, state, false, level + 1, if_expr, stmt_expr)?, pos, )))), // -expr Token::UnaryMinus => { let pos = eat_token(input, Token::UnaryMinus); - match parse_unary(input, state, level + 1, allow_stmt_expr)? { + match parse_unary(input, state, level + 1, if_expr, stmt_expr)? { // Negative integer Expr::IntegerConstant(x) => { let (num, pos) = *x; @@ -1369,13 +1389,14 @@ fn parse_unary<'a>( // +expr Token::UnaryPlus => { eat_token(input, Token::UnaryPlus); - parse_unary(input, state, level + 1, allow_stmt_expr) + parse_unary(input, state, level + 1, if_expr, stmt_expr) } // !expr Token::Bang => { let pos = eat_token(input, Token::Bang); let mut args = StaticVec::new(); - args.push(parse_primary(input, state, level + 1, allow_stmt_expr)?); + let expr = parse_primary(input, state, level + 1, if_expr, stmt_expr)?; + args.push(expr); let op = "!"; let hash = calc_fn_hash(empty(), op, 2, empty()); @@ -1391,7 +1412,7 @@ fn parse_unary<'a>( // Token::EOF => Err(PERR::UnexpectedEOF.into_err(pos)), // All other tokens - _ => parse_primary(input, state, level + 1, allow_stmt_expr), + _ => parse_primary(input, state, level + 1, if_expr, stmt_expr), } } @@ -1451,7 +1472,8 @@ fn parse_op_assignment_stmt<'a>( state: &mut ParseState, lhs: Expr, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { let (token, pos) = input.peek().unwrap(); let pos = *pos; @@ -1479,7 +1501,7 @@ fn parse_op_assignment_stmt<'a>( }; let (_, pos) = input.next().unwrap(); - let rhs = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let rhs = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; make_assignment_stmt(op, state, lhs, rhs, pos) } @@ -1694,7 +1716,8 @@ fn parse_binary_op<'a>( parent_precedence: u8, lhs: Expr, mut level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { if level > state.max_expr_depth { return Err(PERR::ExprTooDeep.into_err(lhs.position())); @@ -1715,14 +1738,14 @@ fn parse_binary_op<'a>( let (op_token, pos) = input.next().unwrap(); - let rhs = parse_unary(input, state, level, allow_stmt_expr)?; + let rhs = parse_unary(input, state, level, if_expr, stmt_expr)?; let next_precedence = input.peek().unwrap().0.precedence(); // Bind to right if the next operator has higher precedence // If same precedence, then check if the operator binds right let rhs = if (precedence == next_precedence && bind_right) || precedence < next_precedence { - parse_binary_op(input, state, precedence, rhs, level, allow_stmt_expr)? + parse_binary_op(input, state, precedence, rhs, level, if_expr, stmt_expr)? } else { // Otherwise bind to left (even if next operator has the same precedence) rhs @@ -1810,7 +1833,8 @@ fn parse_expr<'a>( input: &mut Peekable>, state: &mut ParseState, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { let (_, pos) = input.peek().unwrap(); @@ -1818,8 +1842,8 @@ fn parse_expr<'a>( return Err(PERR::ExprTooDeep.into_err(*pos)); } - let lhs = parse_unary(input, state, level + 1, allow_stmt_expr)?; - parse_binary_op(input, state, 1, lhs, level + 1, allow_stmt_expr) + let lhs = parse_unary(input, state, level + 1, if_expr, stmt_expr)?; + parse_binary_op(input, state, 1, lhs, level + 1, if_expr, stmt_expr) } /// Make sure that the expression is not a statement expression (i.e. wrapped in `{}`). @@ -1870,7 +1894,8 @@ fn parse_if<'a>( state: &mut ParseState, breakable: bool, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { // if ... let pos = eat_token(input, Token::If); @@ -1881,18 +1906,18 @@ fn parse_if<'a>( // if guard { if_body } ensure_not_statement_expr(input, "a boolean")?; - let guard = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let guard = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; ensure_not_assignment(input)?; - let if_body = parse_block(input, state, breakable, level + 1, allow_stmt_expr)?; + let if_body = parse_block(input, state, breakable, level + 1, if_expr, stmt_expr)?; // if guard { if_body } else ... let else_body = if match_token(input, Token::Else).unwrap_or(false) { Some(if let (Token::If, _) = input.peek().unwrap() { // if guard { if_body } else if ... - parse_if(input, state, breakable, level + 1, allow_stmt_expr)? + parse_if(input, state, breakable, level + 1, if_expr, stmt_expr)? } else { // if guard { if_body } else { else-body } - parse_block(input, state, breakable, level + 1, allow_stmt_expr)? + parse_block(input, state, breakable, level + 1, if_expr, stmt_expr)? }) } else { None @@ -1906,7 +1931,8 @@ fn parse_while<'a>( input: &mut Peekable>, state: &mut ParseState, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { // while ... let pos = eat_token(input, Token::While); @@ -1917,9 +1943,9 @@ fn parse_while<'a>( // while guard { body } ensure_not_statement_expr(input, "a boolean")?; - let guard = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let guard = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; ensure_not_assignment(input)?; - let body = parse_block(input, state, true, level + 1, allow_stmt_expr)?; + let body = parse_block(input, state, true, level + 1, if_expr, stmt_expr)?; Ok(Stmt::While(Box::new((guard, body)))) } @@ -1929,7 +1955,8 @@ fn parse_loop<'a>( input: &mut Peekable>, state: &mut ParseState, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { // loop ... let pos = eat_token(input, Token::Loop); @@ -1939,7 +1966,7 @@ fn parse_loop<'a>( } // loop { body } - let body = parse_block(input, state, true, level + 1, allow_stmt_expr)?; + let body = parse_block(input, state, true, level + 1, if_expr, stmt_expr)?; Ok(Stmt::Loop(Box::new(body))) } @@ -1949,7 +1976,8 @@ fn parse_for<'a>( input: &mut Peekable>, state: &mut ParseState, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { // for ... let pos = eat_token(input, Token::For); @@ -1984,12 +2012,12 @@ fn parse_for<'a>( // for name in expr { body } ensure_not_statement_expr(input, "a boolean")?; - let expr = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let expr = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; let prev_len = state.len(); state.push((name.clone(), ScopeEntryType::Normal)); - let body = parse_block(input, state, true, level + 1, allow_stmt_expr)?; + let body = parse_block(input, state, true, level + 1, if_expr, stmt_expr)?; state.truncate(prev_len); @@ -2002,7 +2030,8 @@ fn parse_let<'a>( state: &mut ParseState, var_type: ScopeEntryType, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { // let/const... (specified in `var_type`) let (_, pos) = input.next().unwrap(); @@ -2021,7 +2050,7 @@ fn parse_let<'a>( // let name = ... if match_token(input, Token::Equals)? { // let name = expr - let init_value = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let init_value = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; match var_type { // let name = expr @@ -2063,7 +2092,8 @@ fn parse_import<'a>( input: &mut Peekable>, state: &mut ParseState, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { // import ... let pos = eat_token(input, Token::Import); @@ -2073,7 +2103,7 @@ fn parse_import<'a>( } // import expr ... - let expr = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let expr = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; // import expr as ... match input.next().unwrap() { @@ -2169,7 +2199,8 @@ fn parse_block<'a>( state: &mut ParseState, breakable: bool, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { // Must start with { let pos = match input.next().unwrap() { @@ -2193,7 +2224,15 @@ fn parse_block<'a>( while !match_token(input, Token::RightBrace)? { // Parse statements inside the block - let stmt = parse_stmt(input, state, breakable, false, level + 1, allow_stmt_expr)?; + let stmt = parse_stmt( + input, + state, + breakable, + false, + level + 1, + if_expr, + stmt_expr, + )?; // See if it needs a terminating semicolon let need_semicolon = !stmt.is_self_terminated(); @@ -2240,7 +2279,8 @@ fn parse_expr_stmt<'a>( input: &mut Peekable>, state: &mut ParseState, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { let (_, pos) = input.peek().unwrap(); @@ -2248,8 +2288,8 @@ fn parse_expr_stmt<'a>( return Err(PERR::ExprTooDeep.into_err(*pos)); } - let expr = parse_expr(input, state, level + 1, allow_stmt_expr)?; - let expr = parse_op_assignment_stmt(input, state, expr, level + 1, allow_stmt_expr)?; + let expr = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; + let expr = parse_op_assignment_stmt(input, state, expr, level + 1, if_expr, stmt_expr)?; Ok(Stmt::Expr(Box::new(expr))) } @@ -2260,7 +2300,8 @@ fn parse_stmt<'a>( breakable: bool, is_global: bool, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { use ScopeEntryType::{Constant, Normal}; @@ -2277,7 +2318,7 @@ fn parse_stmt<'a>( // Semicolon - empty statement Token::SemiColon => Ok(Stmt::Noop(*pos)), - Token::LeftBrace => parse_block(input, state, breakable, level + 1, allow_stmt_expr), + Token::LeftBrace => parse_block(input, state, breakable, level + 1, if_expr, stmt_expr), // fn ... #[cfg(not(feature = "no_function"))] @@ -2285,10 +2326,10 @@ fn parse_stmt<'a>( #[cfg(not(feature = "no_function"))] Token::Fn => unreachable!(), - Token::If => parse_if(input, state, breakable, level + 1, allow_stmt_expr), - Token::While => parse_while(input, state, level + 1, allow_stmt_expr), - Token::Loop => parse_loop(input, state, level + 1, allow_stmt_expr), - Token::For => parse_for(input, state, level + 1, allow_stmt_expr), + Token::If => parse_if(input, state, breakable, level + 1, if_expr, stmt_expr), + Token::While => parse_while(input, state, level + 1, if_expr, stmt_expr), + Token::Loop => parse_loop(input, state, level + 1, if_expr, stmt_expr), + Token::For => parse_for(input, state, level + 1, if_expr, stmt_expr), Token::Continue if breakable => { let pos = eat_token(input, Token::Continue); @@ -2318,7 +2359,7 @@ fn parse_stmt<'a>( } // `return` or `throw` with expression (_, _) => { - let expr = parse_expr(input, state, level + 1, allow_stmt_expr)?; + let expr = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; let pos = expr.position(); Ok(Stmt::ReturnWithVal(Box::new(( @@ -2329,9 +2370,9 @@ fn parse_stmt<'a>( } } - Token::Let => parse_let(input, state, Normal, level + 1, allow_stmt_expr), - Token::Const => parse_let(input, state, Constant, level + 1, allow_stmt_expr), - Token::Import => parse_import(input, state, level + 1, allow_stmt_expr), + Token::Let => parse_let(input, state, Normal, level + 1, if_expr, stmt_expr), + Token::Const => parse_let(input, state, Constant, level + 1, if_expr, stmt_expr), + Token::Import => parse_import(input, state, level + 1, if_expr, stmt_expr), #[cfg(not(feature = "no_module"))] Token::Export if !is_global => Err(PERR::WrongExport.into_err(*pos)), @@ -2339,7 +2380,7 @@ fn parse_stmt<'a>( #[cfg(not(feature = "no_module"))] Token::Export => parse_export(input, state, level + 1), - _ => parse_expr_stmt(input, state, level + 1, allow_stmt_expr), + _ => parse_expr_stmt(input, state, level + 1, if_expr, stmt_expr), } } @@ -2350,7 +2391,8 @@ fn parse_fn<'a>( state: &mut ParseState, access: FnAccess, level: usize, - allow_stmt_expr: bool, + if_expr: bool, + stmt_expr: bool, ) -> Result { let pos = eat_token(input, Token::Fn); @@ -2421,7 +2463,7 @@ fn parse_fn<'a>( // Parse function body let body = match input.peek().unwrap() { - (Token::LeftBrace, _) => parse_block(input, state, false, level + 1, allow_stmt_expr)?, + (Token::LeftBrace, _) => parse_block(input, state, false, level + 1, if_expr, stmt_expr)?, (_, pos) => return Err(PERR::FnMissingBody(name).into_err(*pos)), }; @@ -2444,7 +2486,7 @@ pub fn parse_global_expr<'a>( max_expr_depth: usize, ) -> Result { let mut state = ParseState::new(max_expr_depth); - let expr = parse_expr(input, &mut state, 0, false)?; + let expr = parse_expr(input, &mut state, 0, false, false)?; match input.peek().unwrap() { (Token::EOF, _) => (), @@ -2489,7 +2531,7 @@ fn parse_global_level<'a>( #[cfg(not(feature = "no_function"))] (Token::Fn, _) => { let mut state = ParseState::new(max_expr_depth.1); - let func = parse_fn(input, &mut state, access, 0, true)?; + let func = parse_fn(input, &mut state, access, 0, true, true)?; // Qualifiers (none) + function name + number of arguments. let hash = calc_fn_hash(empty(), &func.name, func.params.len(), empty()); @@ -2508,7 +2550,7 @@ fn parse_global_level<'a>( } } // Actual statement - let stmt = parse_stmt(input, &mut state, false, true, 0, true)?; + let stmt = parse_stmt(input, &mut state, false, true, 0, true, true)?; let need_semicolon = !stmt.is_self_terminated(); diff --git a/tests/expressions.rs b/tests/expressions.rs index 35a6b90c..75790f00 100644 --- a/tests/expressions.rs +++ b/tests/expressions.rs @@ -12,10 +12,9 @@ fn test_expressions() -> Result<(), Box> { engine.eval_expression_with_scope::(&mut scope, "2 + (x + 10) * 2")?, 42 ); - assert_eq!( - engine.eval_expression_with_scope::(&mut scope, "if x > 0 { 42 } else { 123 }")?, - 42 - ); + assert!(engine + .eval_expression_with_scope::(&mut scope, "if x > 0 { 42 } else { 123 }") + .is_err()); assert!(engine.eval_expression::<()>("40 + 2;").is_err()); assert!(engine.eval_expression::<()>("40 + { 2 }").is_err()); From 37135e25511caea3fed5071f07916e94c4c519f9 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 1 Jun 2020 14:28:39 +0800 Subject: [PATCH 09/53] Modify list formatting according to GitHub MD rules. --- RELEASES.md | 46 +++++++++++++--------------------------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 2bb7b783..05da1a88 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -18,26 +18,18 @@ Bug fixes * Indexing with an index or dot expression now works property (it compiled wrongly before). For example, `let s = "hello"; s[s.len-1] = 'x';` now works property instead of causing a runtime error. +* `if` expressions are not supposed to be allowed when compiling for expressions only. This is fixed. Breaking changes ---------------- * `Engine::compile_XXX` functions now return `ParseError` instead of `Box`. -* The `RegisterDynamicFn` trait is merged into the `RegisterResultFn` trait which now always returns - `Result>`. +* The `RegisterDynamicFn` trait is merged into the `RegisterResultFn` trait which now always returns `Result>`. * Default maximum limit on levels of nested function calls is fine-tuned and set to a different value. -* Some operator functions are now built in (see _Speed enhancements_ below), so they are available even - under `Engine::new_raw`. -* Strings are now immutable. The type `rhai::ImmutableString` is used instead of `std::string::String`. - This is to avoid excessive cloning of strings. All native-Rust functions taking string parameters - should switch to `rhai::ImmutableString` (which is either `Rc` or `Arc` depending on - whether the `sync` feature is used). -* Native Rust functions registered with the `Engine` also mutates the first argument when called in - normal function-call style (previously the first argument will be passed by _value_ if not called - in method-call style). Of course, if the first argument is a calculated value (e.g. result of an - expression), then mutating it has no effect, but at least it is not cloned. -* Some built-in methods (e.g. `len` for string, `floor` for `FLOAT`) now have _property_ versions in - addition to methods to simplify coding. +* Some operator functions are now built in (see _Speed enhancements_ below), so they are available even under `Engine::new_raw`. +* Strings are now immutable. The type `rhai::ImmutableString` is used instead of `std::string::String`. This is to avoid excessive cloning of strings. All native-Rust functions taking string parameters should switch to `rhai::ImmutableString` (which is either `Rc` or `Arc` depending on whether the `sync` feature is used). +* Native Rust functions registered with the `Engine` also mutates the first argument when called in normal function-call style (previously the first argument will be passed by _value_ if not called in method-call style). Of course, if the first argument is a calculated value (e.g. result of an expression), then mutating it has no effect, but at least it is not cloned. +* Some built-in methods (e.g. `len` for string, `floor` for `FLOAT`) now have _property_ versions in addition to methods to simplify coding. New features ------------ @@ -50,23 +42,13 @@ New features Speed enhancements ------------------ -* Common operators (e.g. `+`, `>`, `==`) now call into highly efficient built-in implementations for standard types - (i.e. `INT`, `FLOAT`, `bool`, `char`, `()` and `ImmutableString`) if not overridden by a registered function. - This yields a 5-10% speed benefit depending on script operator usage. Scripts running tight loops will see - significant speed-up. -* Common assignment operators (e.g. `+=`, `%=`) now call into highly efficient built-in implementations for - standard types (i.e. `INT`, `FLOAT`, `bool`, `char`, `()` and `ImmutableString`) if not overridden by a registered function. -* Implementations of common operators for standard types are removed from the `ArithmeticPackage` and `LogicPackage` - (and therefore the `CorePackage`) because they are now always available, even under `Engine::new_raw`. +* Common operators (e.g. `+`, `>`, `==`) now call into highly efficient built-in implementations for standard types (i.e. `INT`, `FLOAT`, `bool`, `char`, `()` and `ImmutableString`) if not overridden by a registered function. This yields a 5-10% speed benefit depending on script operator usage. Scripts running tight loops will see significant speed-up. +* Common assignment operators (e.g. `+=`, `%=`) now call into highly efficient built-in implementations for standard types (i.e. `INT`, `FLOAT`, `bool`, `char`, `()` and `ImmutableString`) if not overridden by a registered function. +* Implementations of common operators for standard types are removed from the `ArithmeticPackage` and `LogicPackage` (and therefore the `CorePackage`) because they are now always available, even under `Engine::new_raw`. * Operator-assignment statements (e.g. `+=`) are now handled directly and much faster. * Strings are now _immutable_ and use the `rhai::ImmutableString` type, eliminating large amounts of cloning. -* For Native Rust functions taking a first `&mut` parameter, the first argument is passed by reference instead of - by value, even if not called in method-call style. This allows many functions declared with `&mut` parameter to avoid - excessive cloning. For example, if `a` is a large array, getting its length in this manner: `len(a)` used to result - in a full clone of `a` before taking the length and throwing the copy away. Now, `a` is simply passed by reference, - avoiding the cloning altogether. -* A custom hasher simply passes through `u64` keys without hashing to avoid function call hash keys - (which are by themselves `u64`) being hashed twice. +* For Native Rust functions taking a first `&mut` parameter, the first argument is passed by reference instead of by value, even if not called in method-call style. This allows many functions declared with `&mut` parameter to avoid excessive cloning. For example, if `a` is a large array, getting its length in this manner: `len(a)` used to result in a full clone of `a` before taking the length and throwing the copy away. Now, `a` is simply passed by reference, avoiding the cloning altogether. +* A custom hasher simply passes through `u64` keys without hashing to avoid function call hash keys (which are by themselves `u64`) being hashed twice. Version 0.14.1 @@ -78,15 +60,13 @@ The major features for this release is modules, script resource limits, and spee New features ------------ -* Modules and _module resolvers_ allow loading external scripts under a module namespace. - A module can contain constant variables, Rust functions and Rhai functions. +* Modules and _module resolvers_ allow loading external scripts under a module namespace. A module can contain constant variables, Rust functions and Rhai functions. * `export` variables and `private` functions. * _Indexers_ for Rust types. * Track script evaluation progress and terminate script run. * Set limit on maximum number of operations allowed per script run. * Set limit on maximum number of modules loaded per script run. -* A new API, `Engine::compile_scripts_with_scope`, can compile a list of script segments without needing to - first concatenate them together into one large string. +* A new API, `Engine::compile_scripts_with_scope`, can compile a list of script segments without needing to first concatenate them together into one large string. * Stepped `range` function with a custom step. Speed improvements From b70d38e820cc29a367d08232a7b03a57c02f273f Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 1 Jun 2020 15:25:22 +0800 Subject: [PATCH 10/53] Avoid passing position until error. --- src/api.rs | 10 +- src/engine.rs | 242 ++++++++++++++++++++++++++---------------------- src/optimize.rs | 40 ++++---- src/result.rs | 8 +- 4 files changed, 163 insertions(+), 137 deletions(-) diff --git a/src/api.rs b/src/api.rs index 1d593023..6afc7a35 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1104,16 +1104,20 @@ impl Engine { ) -> Result> { let mut args: StaticVec<_> = arg_values.iter_mut().collect(); let lib = ast.lib(); - let pos = Position::none(); let fn_def = lib .get_function_by_signature(name, args.len(), true) - .ok_or_else(|| Box::new(EvalAltResult::ErrorFunctionNotFound(name.into(), pos)))?; + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorFunctionNotFound( + name.into(), + Position::none(), + )) + })?; let mut state = State::new(); let args = args.as_mut(); - self.call_script_fn(scope, &mut state, &lib, name, fn_def, args, pos, 0) + self.call_script_fn(scope, &mut state, &lib, name, fn_def, args, 0) } /// Optimize the `AST` with constants defined in an external Scope. diff --git a/src/engine.rs b/src/engine.rs index 453ee184..21561e70 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -116,17 +116,20 @@ impl Target<'_> { } /// Update the value of the `Target`. - pub fn set_value(&mut self, new_val: Dynamic, pos: Position) -> Result<(), Box> { + /// Position in `EvalAltResult` is None and must be set afterwards. + pub fn set_value(&mut self, new_val: Dynamic) -> Result<(), Box> { match self { Target::Ref(r) => **r = new_val, Target::Value(_) => { - return Err(Box::new(EvalAltResult::ErrorAssignmentToUnknownLHS(pos))) + return Err(Box::new(EvalAltResult::ErrorAssignmentToUnknownLHS( + Position::none(), + ))) } Target::StringChar(Dynamic(Union::Str(ref mut s)), index, _) => { // Replace the character at the specified index position let new_ch = new_val .as_char() - .map_err(|_| EvalAltResult::ErrorCharMismatch(pos))?; + .map_err(|_| EvalAltResult::ErrorCharMismatch(Position::none()))?; let mut chars: StaticVec = s.chars().collect(); let ch = chars[*index]; @@ -575,6 +578,7 @@ impl Engine { } /// Universal method for calling functions either registered with the `Engine` or written in Rhai. + /// Position in `EvalAltResult` is None and must be set afterwards. /// /// ## WARNING /// @@ -591,10 +595,9 @@ impl Engine { args: &mut FnCallArgs, is_ref: bool, def_val: Option<&Dynamic>, - pos: Position, level: usize, ) -> Result<(Dynamic, bool), Box> { - self.inc_operations(state, pos)?; + self.inc_operations(state)?; let native_only = hashes.1 == 0; @@ -603,7 +606,9 @@ impl Engine { #[cfg(not(feature = "unchecked"))] { if level > self.max_call_stack_depth { - return Err(Box::new(EvalAltResult::ErrorStackOverflow(pos))); + return Err(Box::new( + EvalAltResult::ErrorStackOverflow(Position::none()), + )); } } @@ -650,7 +655,7 @@ impl Engine { // Run scripted function let result = - self.call_script_fn(scope, state, lib, fn_name, fn_def, args, pos, level)?; + self.call_script_fn(scope, state, lib, fn_name, fn_def, args, level)?; // Restore the original reference restore_first_arg(old_this_ptr, args); @@ -674,20 +679,18 @@ impl Engine { ); // Run external function - let result = func.get_native_fn()(args); + let result = func.get_native_fn()(args)?; // Restore the original reference restore_first_arg(old_this_ptr, args); - let result = result.map_err(|err| err.new_position(pos))?; - // See if the function match print/debug (which requires special processing) return Ok(match fn_name { KEYWORD_PRINT => ( (self.print)(result.as_str().map_err(|type_name| { Box::new(EvalAltResult::ErrorMismatchOutputType( type_name.into(), - pos, + Position::none(), )) })?) .into(), @@ -697,7 +700,7 @@ impl Engine { (self.debug)(result.as_str().map_err(|type_name| { Box::new(EvalAltResult::ErrorMismatchOutputType( type_name.into(), - pos, + Position::none(), )) })?) .into(), @@ -724,7 +727,7 @@ impl Engine { if let Some(prop) = extract_prop_from_getter(fn_name) { return Err(Box::new(EvalAltResult::ErrorDotExpr( format!("- property '{}' unknown or write-only", prop), - pos, + Position::none(), ))); } @@ -732,7 +735,7 @@ impl Engine { if let Some(prop) = extract_prop_from_setter(fn_name) { return Err(Box::new(EvalAltResult::ErrorDotExpr( format!("- property '{}' unknown or read-only", prop), - pos, + Position::none(), ))); } @@ -745,18 +748,19 @@ impl Engine { if fn_name == FUNC_INDEXER { return Err(Box::new(EvalAltResult::ErrorFunctionNotFound( format!("[]({})", types_list.join(", ")), - pos, + Position::none(), ))); } // Raise error Err(Box::new(EvalAltResult::ErrorFunctionNotFound( format!("{} ({})", fn_name, types_list.join(", ")), - pos, + Position::none(), ))) } /// Call a script-defined function. + /// Position in `EvalAltResult` is None and must be set afterwards. /// /// ## WARNING /// @@ -771,7 +775,6 @@ impl Engine { fn_name: &str, fn_def: &FnDef, args: &mut FnCallArgs, - pos: Position, level: usize, ) -> Result> { let orig_scope_level = state.scope_level; @@ -798,13 +801,17 @@ impl Engine { .or_else(|err| match *err { // Convert return statement to return value EvalAltResult::Return(x, _) => Ok(x), - EvalAltResult::ErrorInFunctionCall(name, err, _) => Err(Box::new( - EvalAltResult::ErrorInFunctionCall(format!("{} > {}", fn_name, name), err, pos), - )), + EvalAltResult::ErrorInFunctionCall(name, err, _) => { + Err(Box::new(EvalAltResult::ErrorInFunctionCall( + format!("{} > {}", fn_name, name), + err, + Position::none(), + ))) + } _ => Err(Box::new(EvalAltResult::ErrorInFunctionCall( fn_name.to_string(), err, - pos, + Position::none(), ))), }); @@ -825,7 +832,8 @@ impl Engine { || lib.contains_key(&hashes.1) } - // Perform an actual function call, taking care of special functions + /// Perform an actual function call, taking care of special functions + /// Position in `EvalAltResult` is None and must be set afterwards. /// /// ## WARNING /// @@ -842,7 +850,6 @@ impl Engine { args: &mut FnCallArgs, is_ref: bool, def_val: Option<&Dynamic>, - pos: Position, level: usize, ) -> Result<(Dynamic, bool), Box> { // Qualifiers (none) + function name + number of arguments + argument `TypeId`'s. @@ -865,7 +872,7 @@ impl Engine { KEYWORD_EVAL if args.len() == 1 && !self.has_override(lib, hashes) => { Err(Box::new(EvalAltResult::ErrorRuntime( "'eval' should not be called in method style. Try eval(...);".into(), - pos, + Position::none(), ))) } @@ -873,24 +880,24 @@ impl Engine { _ => { let mut scope = Scope::new(); self.call_fn_raw( - &mut scope, state, lib, fn_name, hashes, args, is_ref, def_val, pos, level, + &mut scope, state, lib, fn_name, hashes, args, is_ref, def_val, level, ) } } } /// Evaluate a text string as a script - used primarily for 'eval'. + /// Position in `EvalAltResult` is None and must be set afterwards. fn eval_script_expr( &self, scope: &mut Scope, state: &mut State, lib: &FunctionsLib, script: &Dynamic, - pos: Position, ) -> Result> { - let script = script - .as_str() - .map_err(|type_name| EvalAltResult::ErrorMismatchOutputType(type_name.into(), pos))?; + let script = script.as_str().map_err(|type_name| { + EvalAltResult::ErrorMismatchOutputType(type_name.into(), Position::none()) + })?; // Compile the script text // No optimizations because we only run it once @@ -903,7 +910,7 @@ impl Engine { // If new functions are defined within the eval string, it is an error if !ast.lib().is_empty() { return Err(Box::new(EvalAltResult::ErrorParsing( - ParseErrorType::WrongFnDefinition.into_err(pos), + ParseErrorType::WrongFnDefinition.into_err(Position::none()), ))); } @@ -911,17 +918,16 @@ impl Engine { let ast = AST::new(statements, lib.clone()); // Evaluate the AST - let (result, operations) = self - .eval_ast_with_scope_raw(scope, &ast) - .map_err(|err| err.new_position(pos))?; + let (result, operations) = self.eval_ast_with_scope_raw(scope, &ast)?; state.operations += operations; - self.inc_operations(state, pos)?; + self.inc_operations(state)?; return Ok(result); } /// Chain-evaluate a dot/index chain. + /// Position in `EvalAltResult` is None and must be set afterwards. fn eval_dot_index_chain_helper( &self, state: &mut State, @@ -930,7 +936,6 @@ impl Engine { rhs: &Expr, idx_values: &mut StaticVec, is_index: bool, - op_pos: Position, level: usize, mut new_val: Option, ) -> Result<(Dynamic, bool), Box> { @@ -951,25 +956,27 @@ impl Engine { let (idx, expr, pos) = x.as_ref(); let is_idx = matches!(rhs, Expr::Index(_)); let idx_pos = idx.position(); - let this_ptr = &mut self.get_indexed_mut( - state, lib, obj, is_ref, idx_val, idx_pos, op_pos, false, - )?; + let this_ptr = &mut self + .get_indexed_mut(state, lib, obj, is_ref, idx_val, idx_pos, false)?; self.eval_dot_index_chain_helper( - state, lib, this_ptr, expr, idx_values, is_idx, *pos, level, new_val, + state, lib, this_ptr, expr, idx_values, is_idx, level, new_val, ) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } // xxx[rhs] = new_val _ if new_val.is_some() => { - let this_ptr = &mut self - .get_indexed_mut(state, lib, obj, is_ref, idx_val, pos, op_pos, true)?; + let this_ptr = + &mut self.get_indexed_mut(state, lib, obj, is_ref, idx_val, pos, true)?; - this_ptr.set_value(new_val.unwrap(), rhs.position())?; + this_ptr + .set_value(new_val.unwrap()) + .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; Ok((Default::default(), true)) } // xxx[rhs] _ => self - .get_indexed_mut(state, lib, obj, is_ref, idx_val, pos, op_pos, false) + .get_indexed_mut(state, lib, obj, is_ref, idx_val, pos, false) .map(|v| (v.clone_into_dynamic(), false)), } } else { @@ -989,9 +996,8 @@ impl Engine { .collect(); let args = arg_values.as_mut(); - self.exec_fn_call( - state, lib, name, *native, *hash, args, is_ref, def_val, *pos, 0, - ) + self.exec_fn_call(state, lib, name, *native, *hash, args, is_ref, def_val, 0) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } // xxx.module::fn_name(...) - syntax error Expr::FnCall(_) => unreachable!(), @@ -1001,9 +1007,10 @@ impl Engine { let ((prop, _, _), pos) = x.as_ref(); let index = prop.clone().into(); let mut val = - self.get_indexed_mut(state, lib, obj, is_ref, index, *pos, op_pos, true)?; + self.get_indexed_mut(state, lib, obj, is_ref, index, *pos, true)?; - val.set_value(new_val.unwrap(), rhs.position())?; + val.set_value(new_val.unwrap()) + .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; Ok((Default::default(), true)) } // {xxx:map}.id @@ -1011,8 +1018,7 @@ impl Engine { Expr::Property(x) if obj.is::() => { let ((prop, _, _), pos) = x.as_ref(); let index = prop.clone().into(); - let val = - self.get_indexed_mut(state, lib, obj, is_ref, index, *pos, op_pos, false)?; + let val = self.get_indexed_mut(state, lib, obj, is_ref, index, *pos, false)?; Ok((val.clone_into_dynamic(), false)) } @@ -1020,19 +1026,17 @@ impl Engine { Expr::Property(x) if new_val.is_some() => { let ((_, _, setter), pos) = x.as_ref(); let mut args = [obj, new_val.as_mut().unwrap()]; - self.exec_fn_call( - state, lib, setter, true, 0, &mut args, is_ref, None, *pos, 0, - ) - .map(|(v, _)| (v, true)) + self.exec_fn_call(state, lib, setter, true, 0, &mut args, is_ref, None, 0) + .map(|(v, _)| (v, true)) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } // xxx.id Expr::Property(x) => { let ((_, getter, _), pos) = x.as_ref(); let mut args = [obj]; - self.exec_fn_call( - state, lib, getter, true, 0, &mut args, is_ref, None, *pos, 0, - ) - .map(|(v, _)| (v, false)) + self.exec_fn_call(state, lib, getter, true, 0, &mut args, is_ref, None, 0) + .map(|(v, _)| (v, false)) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } #[cfg(not(feature = "no_object"))] // {xxx:map}.prop[expr] | {xxx:map}.prop.expr @@ -1043,14 +1047,15 @@ impl Engine { let mut val = if let Expr::Property(p) = prop { let ((prop, _, _), _) = p.as_ref(); let index = prop.clone().into(); - self.get_indexed_mut(state, lib, obj, is_ref, index, *pos, op_pos, false)? + self.get_indexed_mut(state, lib, obj, is_ref, index, *pos, false)? } else { unreachable!(); }; self.eval_dot_index_chain_helper( - state, lib, &mut val, expr, idx_values, is_idx, *pos, level, new_val, + state, lib, &mut val, expr, idx_values, is_idx, level, new_val, ) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } // xxx.prop[expr] | xxx.prop.expr Expr::Index(x) | Expr::Dot(x) => { @@ -1061,16 +1066,19 @@ impl Engine { let (mut val, updated) = if let Expr::Property(p) = prop { let ((_, getter, _), _) = p.as_ref(); let args = &mut args[..1]; - self.exec_fn_call(state, lib, getter, true, 0, args, is_ref, None, *pos, 0)? + self.exec_fn_call(state, lib, getter, true, 0, args, is_ref, None, 0) + .map_err(|err| EvalAltResult::new_position(err, *pos))? } else { unreachable!(); }; let val = &mut val; let target = &mut val.into(); - let (result, may_be_changed) = self.eval_dot_index_chain_helper( - state, lib, target, expr, idx_values, is_idx, *pos, level, new_val, - )?; + let (result, may_be_changed) = self + .eval_dot_index_chain_helper( + state, lib, target, expr, idx_values, is_idx, level, new_val, + ) + .map_err(|err| EvalAltResult::new_position(err, *pos))?; // Feed the value back via a setter just in case it has been updated if updated || may_be_changed { @@ -1078,14 +1086,12 @@ impl Engine { let ((_, _, setter), _) = p.as_ref(); // Re-use args because the first &mut parameter will not be consumed args[1] = val; - self.exec_fn_call( - state, lib, setter, true, 0, args, is_ref, None, *pos, 0, - ) - .or_else(|err| match *err { - // If there is no setter, no need to feed it back because the property is read-only - EvalAltResult::ErrorDotExpr(_, _) => Ok(Default::default()), - err => Err(Box::new(err)), - })?; + self.exec_fn_call(state, lib, setter, true, 0, args, is_ref, None, 0) + .or_else(|err| match *err { + // If there is no setter, no need to feed it back because the property is read-only + EvalAltResult::ErrorDotExpr(_, _) => Ok(Default::default()), + err => Err(EvalAltResult::new_position(Box::new(err), *pos)), + })?; } } @@ -1124,7 +1130,8 @@ impl Engine { // id.??? or id[???] Expr::Variable(_) => { let (target, name, typ, pos) = search_scope(scope, state, dot_lhs)?; - self.inc_operations(state, pos)?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, pos))?; // Constants cannot be modified match typ { @@ -1140,9 +1147,10 @@ impl Engine { let this_ptr = &mut target.into(); self.eval_dot_index_chain_helper( - state, lib, this_ptr, dot_rhs, idx_values, is_index, *op_pos, level, new_val, + state, lib, this_ptr, dot_rhs, idx_values, is_index, level, new_val, ) .map(|(v, _)| v) + .map_err(|err| EvalAltResult::new_position(err, *op_pos)) } // {expr}.??? = ??? or {expr}[???] = ??? expr if new_val.is_some() => { @@ -1155,9 +1163,10 @@ impl Engine { let val = self.eval_expr(scope, state, lib, expr, level)?; let this_ptr = &mut val.into(); self.eval_dot_index_chain_helper( - state, lib, this_ptr, dot_rhs, idx_values, is_index, *op_pos, level, new_val, + state, lib, this_ptr, dot_rhs, idx_values, is_index, level, new_val, ) .map(|(v, _)| v) + .map_err(|err| EvalAltResult::new_position(err, *op_pos)) } } } @@ -1177,7 +1186,8 @@ impl Engine { size: usize, level: usize, ) -> Result<(), Box> { - self.inc_operations(state, expr.position())?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, expr.position()))?; match expr { Expr::FnCall(x) if x.1.is_none() => { @@ -1211,6 +1221,7 @@ impl Engine { } /// Get the value at the indexed position of a base type + /// Position in `EvalAltResult` may be None and should be set afterwards. fn get_indexed_mut<'a>( &self, state: &mut State, @@ -1219,10 +1230,9 @@ impl Engine { is_ref: bool, mut idx: Dynamic, idx_pos: Position, - op_pos: Position, create: bool, ) -> Result, Box> { - self.inc_operations(state, op_pos)?; + self.inc_operations(state)?; match val { #[cfg(not(feature = "no_index"))] @@ -1292,10 +1302,13 @@ impl Engine { let fn_name = FUNC_INDEXER; let type_name = self.map_type_name(val.type_name()); let args = &mut [val, &mut idx]; - self.exec_fn_call(state, lib, fn_name, true, 0, args, is_ref, None, op_pos, 0) + self.exec_fn_call(state, lib, fn_name, true, 0, args, is_ref, None, 0) .map(|(v, _)| v.into()) .map_err(|_| { - Box::new(EvalAltResult::ErrorIndexingType(type_name.into(), op_pos)) + Box::new(EvalAltResult::ErrorIndexingType( + type_name.into(), + Position::none(), + )) }) } } @@ -1311,7 +1324,8 @@ impl Engine { rhs: &Expr, level: usize, ) -> Result> { - self.inc_operations(state, rhs.position())?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; let lhs_value = self.eval_expr(scope, state, lib, lhs, level)?; let rhs_value = self.eval_expr(scope, state, lib, rhs, level)?; @@ -1327,7 +1341,6 @@ impl Engine { for value in rhs_value.iter_mut() { let args = &mut [&mut lhs_value.clone(), value]; let def_value = Some(&def_value); - let pos = rhs.position(); let hashes = ( // Qualifiers (none) + function name + number of arguments + argument `TypeId`'s. @@ -1335,9 +1348,11 @@ impl Engine { 0, ); - let (r, _) = self.call_fn_raw( - &mut scope, state, lib, op, hashes, args, false, def_value, pos, level, - )?; + let (r, _) = self + .call_fn_raw( + &mut scope, state, lib, op, hashes, args, false, def_value, level, + ) + .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; if r.as_bool().unwrap_or(false) { return Ok(true.into()); } @@ -1371,7 +1386,8 @@ impl Engine { expr: &Expr, level: usize, ) -> Result> { - self.inc_operations(state, expr.position())?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, expr.position()))?; match expr { Expr::Expr(x) => self.eval_expr(scope, state, lib, x.as_ref(), level), @@ -1395,7 +1411,8 @@ impl Engine { let (lhs_expr, op, rhs_expr, op_pos) = x.as_ref(); let mut rhs_val = self.eval_expr(scope, state, lib, rhs_expr, level)?; let (lhs_ptr, name, typ, pos) = search_scope(scope, state, lhs_expr)?; - self.inc_operations(state, pos)?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, pos))?; match typ { // Assignment to constant variable @@ -1432,10 +1449,9 @@ impl Engine { // Set variable value *lhs_ptr = self - .exec_fn_call( - state, lib, op, true, hash, args, false, None, *op_pos, level, - ) - .map(|(v, _)| v)?; + .exec_fn_call(state, lib, op, true, hash, args, false, None, level) + .map(|(v, _)| v) + .map_err(|err| EvalAltResult::new_position(err, *op_pos))?; } Ok(Default::default()) } @@ -1460,10 +1476,9 @@ impl Engine { &mut self.eval_expr(scope, state, lib, lhs_expr, level)?, &mut rhs_val, ]; - self.exec_fn_call( - state, lib, op, true, hash, args, false, None, *op_pos, level, - ) - .map(|(v, _)| v)? + self.exec_fn_call(state, lib, op, true, hash, args, false, None, level) + .map(|(v, _)| v) + .map_err(|err| EvalAltResult::new_position(err, *op_pos))? }); match lhs_expr { @@ -1531,11 +1546,11 @@ impl Engine { if !self.has_override(lib, (hash_fn, *hash)) { // eval - only in function call style let prev_len = scope.len(); - let pos = args_expr.get(0).position(); - - // Evaluate the text string as a script - let script = self.eval_expr(scope, state, lib, args_expr.get(0), level)?; - let result = self.eval_script_expr(scope, state, lib, &script, pos); + let expr = args_expr.get(0); + let script = self.eval_expr(scope, state, lib, expr, level)?; + let result = self + .eval_script_expr(scope, state, lib, &script) + .map_err(|err| EvalAltResult::new_position(err, expr.position())); if scope.len() != prev_len { // IMPORTANT! If the eval defines new variables in the current scope, @@ -1568,7 +1583,8 @@ impl Engine { .collect::>()?; let (target, _, typ, pos) = search_scope(scope, state, lhs)?; - self.inc_operations(state, pos)?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, pos))?; match typ { ScopeEntryType::Module => unreachable!(), @@ -1593,9 +1609,10 @@ impl Engine { let args = args.as_mut(); self.exec_fn_call( - state, lib, name, *native, *hash, args, is_ref, def_val, *pos, level, + state, lib, name, *native, *hash, args, is_ref, def_val, level, ) .map(|(v, _)| v) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } // Module-qualified function call @@ -1628,7 +1645,8 @@ impl Engine { let func = match module.get_qualified_fn(name, *hash_fn_def) { Err(err) if matches!(*err, EvalAltResult::ErrorFunctionNotFound(_, _)) => { // Then search in Rust functions - self.inc_operations(state, *pos)?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, *pos))?; // Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, @@ -1650,7 +1668,8 @@ impl Engine { let args = args.as_mut(); let fn_def = x.get_fn_def(); let mut scope = Scope::new(); - self.call_script_fn(&mut scope, state, lib, name, fn_def, args, *pos, level) + self.call_script_fn(&mut scope, state, lib, name, fn_def, args, level) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } Ok(x) => x.get_native_fn()(args.as_mut()).map_err(|err| err.new_position(*pos)), Err(err) @@ -1718,7 +1737,8 @@ impl Engine { stmt: &Stmt, level: usize, ) -> Result> { - self.inc_operations(state, stmt.position())?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, stmt.position()))?; match stmt { // No-op @@ -1823,7 +1843,8 @@ impl Engine { for loop_var in func(iter_type) { *scope.get_mut(index).0 = loop_var; - self.inc_operations(state, stmt.position())?; + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, stmt.position()))?; match self.eval_stmt(scope, state, lib, stmt, level) { Ok(_) => (), @@ -1975,14 +1996,17 @@ impl Engine { } /// Check if the number of operations stay within limit. - fn inc_operations(&self, state: &mut State, pos: Position) -> Result<(), Box> { + /// Position in `EvalAltResult` is None and must be set afterwards. + fn inc_operations(&self, state: &mut State) -> Result<(), Box> { state.operations += 1; #[cfg(not(feature = "unchecked"))] { // Guard against too many operations if state.operations > self.max_operations { - return Err(Box::new(EvalAltResult::ErrorTooManyOperations(pos))); + return Err(Box::new(EvalAltResult::ErrorTooManyOperations( + Position::none(), + ))); } } @@ -1990,7 +2014,7 @@ impl Engine { if let Some(progress) = &self.progress { if !progress(state.operations) { // Terminate script if progress returns false - return Err(Box::new(EvalAltResult::ErrorTerminated(pos))); + return Err(Box::new(EvalAltResult::ErrorTerminated(Position::none()))); } } diff --git a/src/optimize.rs b/src/optimize.rs index 78613ace..ea27620d 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -5,9 +5,7 @@ use crate::engine::{ KEYWORD_TYPE_OF, }; use crate::parser::{map_dynamic_to_expr, Expr, FnDef, ReturnType, Stmt, AST}; -use crate::result::EvalAltResult; use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; -use crate::token::Position; use crate::utils::StaticVec; use crate::stdlib::{ @@ -113,8 +111,7 @@ fn call_fn_with_constant_arguments( state: &State, fn_name: &str, arg_values: &mut [Dynamic], - pos: Position, -) -> Result, Box> { +) -> Option { // Search built-in's and external functions let hash_fn = calc_fn_hash( empty(), @@ -134,11 +131,10 @@ fn call_fn_with_constant_arguments( arg_values.iter_mut().collect::>().as_mut(), false, None, - pos, 0, ) .map(|(v, _)| Some(v)) - .or_else(|_| Ok(None)) + .unwrap_or_else(|_| None) } /// Optimize a statement. @@ -574,22 +570,22 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { "" }; - call_fn_with_constant_arguments(&state, name, arg_values.as_mut(), *pos).ok() - .and_then(|result| - result.or_else(|| { - if !arg_for_type_of.is_empty() { - // Handle `type_of()` - Some(arg_for_type_of.to_string().into()) - } else { - // Otherwise use the default value, if any - def_value.clone() - } - }).and_then(|result| map_dynamic_to_expr(result, *pos)) - .map(|expr| { - state.set_dirty(); - expr - }) - ).unwrap_or_else(|| { + call_fn_with_constant_arguments(&state, name, arg_values.as_mut()) + .or_else(|| { + if !arg_for_type_of.is_empty() { + // Handle `type_of()` + Some(arg_for_type_of.to_string().into()) + } else { + // Otherwise use the default value, if any + def_value.clone() + } + }) + .and_then(|result| map_dynamic_to_expr(result, *pos)) + .map(|expr| { + state.set_dirty(); + expr + }) + .unwrap_or_else(|| { // Optimize function call arguments x.3 = x.3.into_iter().map(|a| optimize_expr(a, state)).collect(); Expr::FnCall(x) diff --git a/src/result.rs b/src/result.rs index 2589016b..77102085 100644 --- a/src/result.rs +++ b/src/result.rs @@ -331,10 +331,12 @@ impl EvalAltResult { } } - /// Consume the current `EvalAltResult` and return a new one - /// with the specified `Position`. + /// Consume the current `EvalAltResult` and return a new one with the specified `Position` + /// if the current position is `Position::None`. pub(crate) fn new_position(mut self: Box, new_position: Position) -> Box { - self.set_position(new_position); + if self.position().is_none() { + self.set_position(new_position); + } self } } From b8da1691d3ba06f2e0d7440d1b8979f7e0961954 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 2 Jun 2020 13:33:16 +0800 Subject: [PATCH 11/53] Consolidate callbacks. --- README.md | 2 +- RELEASES.md | 5 +++++ src/api.rs | 4 ++-- src/engine.rs | 30 ++++++++---------------------- src/fn_native.rs | 5 +++++ src/scope.rs | 2 +- src/token.rs | 3 +++ tests/operations.rs | 6 +++--- 8 files changed, 28 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index aaf394fc..1333579b 100644 --- a/README.md +++ b/README.md @@ -2439,7 +2439,7 @@ provide a closure to the `Engine::on_progress` method: ```rust let mut engine = Engine::new(); -engine.on_progress(|count| { // 'count' is the number of operations performed +engine.on_progress(|&count| { // 'count' is the number of operations performed if count % 1000 == 0 { println!("{}", count); // print out a progress log every 1,000 operations } diff --git a/RELEASES.md b/RELEASES.md index 05da1a88..7ba44391 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,6 +4,11 @@ Rhai Release Notes Version 0.16.0 ============== +Breaking changes +---------------- + +* Callback closure passed to `Engine::on_progress` now takes `&u64` instead of `u64` to be consistent with other callback signatures. + Version 0.15.0 ============== diff --git a/src/api.rs b/src/api.rs index 6afc7a35..dc0a26d0 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1163,7 +1163,7 @@ impl Engine { /// /// let mut engine = Engine::new(); /// - /// engine.on_progress(move |ops| { + /// engine.on_progress(move |&ops| { /// if ops > 10000 { /// false /// } else if ops % 800 == 0 { @@ -1182,7 +1182,7 @@ impl Engine { /// # Ok(()) /// # } /// ``` - pub fn on_progress(&mut self, callback: impl Fn(u64) -> bool + SendSync + 'static) { + pub fn on_progress(&mut self, callback: impl Fn(&u64) -> bool + SendSync + 'static) { self.progress = Some(Box::new(callback)); } diff --git a/src/engine.rs b/src/engine.rs index 21561e70..a5001df3 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -3,7 +3,7 @@ use crate::any::{Dynamic, Union}; use crate::calc_fn_hash; use crate::error::ParseErrorType; -use crate::fn_native::{CallableFunction, FnCallArgs, Shared}; +use crate::fn_native::{CallableFunction, Callback, FnCallArgs, Shared}; use crate::module::{resolvers, Module, ModuleResolver}; use crate::optimize::OptimizationLevel; use crate::packages::{CorePackage, Package, PackageLibrary, PackagesCollection, StandardPackage}; @@ -302,26 +302,12 @@ pub struct Engine { /// A hashmap mapping type names to pretty-print names. pub(crate) type_names: HashMap, - /// Closure for implementing the `print` command. - #[cfg(not(feature = "sync"))] - pub(crate) print: Box, - /// Closure for implementing the `print` command. - #[cfg(feature = "sync")] - pub(crate) print: Box, - - /// Closure for implementing the `debug` command. - #[cfg(not(feature = "sync"))] - pub(crate) debug: Box, - /// Closure for implementing the `debug` command. - #[cfg(feature = "sync")] - pub(crate) debug: Box, - - /// Closure for progress reporting. - #[cfg(not(feature = "sync"))] - pub(crate) progress: Option bool + 'static>>, - /// Closure for progress reporting. - #[cfg(feature = "sync")] - pub(crate) progress: Option bool + Send + Sync + 'static>>, + /// Callback closure for implementing the `print` command. + pub(crate) print: Callback, + /// Callback closure for implementing the `debug` command. + pub(crate) debug: Callback, + /// Callback closure for progress reporting. + pub(crate) progress: Option>, /// Optimize the AST after compilation. pub(crate) optimization_level: OptimizationLevel, @@ -2012,7 +1998,7 @@ impl Engine { // Report progress - only in steps if let Some(progress) = &self.progress { - if !progress(state.operations) { + if !progress(&state.operations) { // Terminate script if progress returns false return Err(Box::new(EvalAltResult::ErrorTerminated(Position::none()))); } diff --git a/src/fn_native.rs b/src/fn_native.rs index 6c43adc6..334068b5 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -57,6 +57,11 @@ pub type FnAny = dyn Fn(&mut FnCallArgs) -> Result> pub type IteratorFn = fn(Dynamic) -> Box>; +#[cfg(not(feature = "sync"))] +pub type Callback = Box R + 'static>; +#[cfg(feature = "sync")] +pub type Callback = Box R + Send + Sync + 'static>; + /// A type encapsulating a function callable by Rhai. #[derive(Clone)] pub enum CallableFunction { diff --git a/src/scope.rs b/src/scope.rs index 50a5694f..bb15b777 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -175,7 +175,7 @@ impl<'a> Scope<'a> { /// /// Modules are used for accessing member variables, functions and plugins under a namespace. #[cfg(not(feature = "no_module"))] - pub fn push_module>>(&mut self, name: K, mut value: Module) { + pub fn push_module>>(&mut self, name: K, value: Module) { self.push_module_internal(name, value); } diff --git a/src/token.rs b/src/token.rs index 9e6613b0..2967fc02 100644 --- a/src/token.rs +++ b/src/token.rs @@ -198,6 +198,7 @@ pub enum Token { XOrAssign, ModuloAssign, PowerOfAssign, + #[cfg(not(feature = "no_function"))] Private, Import, #[cfg(not(feature = "no_module"))] @@ -284,6 +285,7 @@ impl Token { ModuloAssign => "%=", PowerOf => "~", PowerOfAssign => "~=", + #[cfg(not(feature = "no_function"))] Private => "private", Import => "import", #[cfg(not(feature = "no_module"))] @@ -757,6 +759,7 @@ impl<'a> TokenIterator<'a> { "throw" => Token::Throw, "for" => Token::For, "in" => Token::In, + #[cfg(not(feature = "no_function"))] "private" => Token::Private, "import" => Token::Import, #[cfg(not(feature = "no_module"))] diff --git a/tests/operations.rs b/tests/operations.rs index c02e2381..91df26c9 100644 --- a/tests/operations.rs +++ b/tests/operations.rs @@ -6,7 +6,7 @@ fn test_max_operations() -> Result<(), Box> { let mut engine = Engine::new(); engine.set_max_operations(500); - engine.on_progress(|count| { + engine.on_progress(|&count| { if count % 100 == 0 { println!("{}", count); } @@ -34,7 +34,7 @@ fn test_max_operations_functions() -> Result<(), Box> { let mut engine = Engine::new(); engine.set_max_operations(500); - engine.on_progress(|count| { + engine.on_progress(|&count| { if count % 100 == 0 { println!("{}", count); } @@ -90,7 +90,7 @@ fn test_max_operations_eval() -> Result<(), Box> { let mut engine = Engine::new(); engine.set_max_operations(500); - engine.on_progress(|count| { + engine.on_progress(|&count| { if count % 100 == 0 { println!("{}", count); } From 27b8f9929d38254e94bd4565361399ae27aab7ed Mon Sep 17 00:00:00 2001 From: jhwgh1968 Date: Tue, 2 Jun 2020 21:44:26 -0500 Subject: [PATCH 12/53] Basic Implementation of Plugins and Plugin Functions --- src/engine.rs | 1 + src/fn_native.rs | 47 +++++++++++++++++++++++++ src/fn_register.rs | 87 ++++++++++++++++++++++++++++++---------------- src/lib.rs | 9 +++-- src/plugin.rs | 42 ++++++++++++++++++++++ 5 files changed, 153 insertions(+), 33 deletions(-) create mode 100644 src/plugin.rs diff --git a/src/engine.rs b/src/engine.rs index 453ee184..d578aaf8 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1652,6 +1652,7 @@ impl Engine { let mut scope = Scope::new(); self.call_script_fn(&mut scope, state, lib, name, fn_def, args, *pos, level) } + Ok(x) if x.is_plugin_fn() => x.get_plugin_fn().call(args.as_mut(), *pos), Ok(x) => x.get_native_fn()(args.as_mut()).map_err(|err| err.new_position(*pos)), Err(err) if def_val.is_some() diff --git a/src/fn_native.rs b/src/fn_native.rs index 6c43adc6..35ddcfdf 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -1,6 +1,7 @@ use crate::any::Dynamic; use crate::parser::FnDef; use crate::result::EvalAltResult; +use crate::token::Position; use crate::stdlib::{boxed::Box, rc::Rc, sync::Arc}; @@ -57,6 +58,13 @@ pub type FnAny = dyn Fn(&mut FnCallArgs) -> Result> pub type IteratorFn = fn(Dynamic) -> Box>; +#[cfg(feature = "sync")] +pub type SharedPluginFunction = Arc; +#[cfg(not(feature = "sync"))] +pub type SharedPluginFunction = Rc; + +use crate::plugin::PluginFunction; + /// A type encapsulating a function callable by Rhai. #[derive(Clone)] pub enum CallableFunction { @@ -69,6 +77,8 @@ pub enum CallableFunction { Iterator(IteratorFn), /// A script-defined function. Script(Shared), + /// A plugin-defined function, + Plugin(SharedPluginFunction), } impl CallableFunction { @@ -77,6 +87,7 @@ impl CallableFunction { match self { Self::Pure(_) => true, Self::Method(_) | Self::Iterator(_) | Self::Script(_) => false, + Self::Plugin(_) => false, } } /// Is this a pure native Rust method-call? @@ -84,6 +95,7 @@ impl CallableFunction { match self { Self::Method(_) => true, Self::Pure(_) | Self::Iterator(_) | Self::Script(_) => false, + Self::Plugin(_) => false, } } /// Is this an iterator function? @@ -91,6 +103,7 @@ impl CallableFunction { match self { Self::Iterator(_) => true, Self::Pure(_) | Self::Method(_) | Self::Script(_) => false, + Self::Plugin(_) => false, } } /// Is this a Rhai-scripted function? @@ -98,6 +111,14 @@ impl CallableFunction { match self { Self::Script(_) => true, Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => false, + Self::Plugin(_) => false, + } + } + /// Is this a plugin-defined function? + pub fn is_plugin_fn(&self) -> bool { + match self { + Self::Plugin(_) => true, + Self::Pure(_) | Self::Method(_) | Self::Iterator(_) | Self::Script(_) => false, } } /// Get a reference to a native Rust function. @@ -109,6 +130,7 @@ impl CallableFunction { match self { Self::Pure(f) | Self::Method(f) => f.as_ref(), Self::Iterator(_) | Self::Script(_) => panic!(), + Self::Plugin(_) => panic!(), } } /// Get a reference to a script-defined function definition. @@ -120,6 +142,7 @@ impl CallableFunction { match self { Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => panic!(), Self::Script(f) => f, + Self::Plugin(_) => panic!(), } } /// Get a reference to an iterator function. @@ -131,6 +154,18 @@ impl CallableFunction { match self { Self::Iterator(f) => *f, Self::Pure(_) | Self::Method(_) | Self::Script(_) => panic!(), + Self::Plugin(_) => panic!(), + } + } + /// Get a reference to a plugin function. + /// + /// # Panics + /// + /// Panics if the `CallableFunction` is not `Plugin`. + pub fn get_plugin_fn<'s>(&'s self) -> SharedPluginFunction { + match self { + Self::Plugin(f) => f.clone(), + Self::Pure(_) | Self::Method(_) | Self::Script(_) | Self::Iterator(_) => panic!(), } } /// Create a new `CallableFunction::Pure`. @@ -141,4 +176,16 @@ impl CallableFunction { pub fn from_method(func: Box) -> Self { Self::Method(func.into()) } + + #[cfg(feature = "sync")] + /// Create a new `CallableFunction::Plugin`. + pub fn from_plugin(plugin: impl PluginFunction + 'static + Send + Sync) -> Self { + Self::Plugin(Arc::new(plugin)) + } + + #[cfg(not(feature = "sync"))] + /// Create a new `CallableFunction::Plugin`. + pub fn from_plugin(plugin: impl PluginFunction + 'static) -> Self { + Self::Plugin(Rc::new(plugin)) + } } diff --git a/src/fn_register.rs b/src/fn_register.rs index f3bfc17c..4d962cf9 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -6,6 +6,7 @@ use crate::any::{Dynamic, Variant}; use crate::engine::Engine; use crate::fn_native::{CallableFunction, FnAny, FnCallArgs}; use crate::parser::FnAccess; +use crate::plugin::Plugin; use crate::result::EvalAltResult; use crate::stdlib::{any::TypeId, boxed::Box, mem}; @@ -13,40 +14,76 @@ use crate::stdlib::{any::TypeId, boxed::Box, mem}; /// A trait to register custom plugins with the `Engine`. /// /// A plugin consists of a number of functions. All functions will be registered with the engine. -#[cfg(feature = "plugins")] -pub trait RegisterPlugin { - /// Register a custom function with the `Engine`. +pub trait RegisterPlugin { + /// Allow extensions of the engine's behavior. + /// + /// This can include importing modules, registering functions to the global name space, and + /// more. /// /// # Example /// /// ``` - /// use rhai::{Dynamic, Engine, INT, Plugin, RegisterDynamicFn, RegisterPlugin}; + /// use rhai::{FLOAT, INT, Module, ModuleResolver, RegisterFn, RegisterPlugin}; + /// use rhai::plugin::*; + /// use rhai::module_resolvers::*; /// - /// // A simple custom plugin type. This should not usually be done with hand-written code. - /// struct AddOffsetPlugin(INT); - /// impl AddOffsetPlugin { - /// fn add_offset(&self, x: INT) -> Dynamic { - /// Dynamic::from(x + self.0) + /// // A function we want to expose to Rhai. + /// #[derive(Copy, Clone)] + /// struct DistanceFunction(); + /// + /// impl PluginFunction for DistanceFunction { + /// fn is_method_call(&self) -> bool { false } + /// fn is_varadic(&self) -> bool { false } + /// + /// fn call(&self, args: &[&mut Dynamic], pos: Position) -> Result> { + /// let x1: &FLOAT = args[0].downcast_ref::().unwrap(); + /// let y1: &FLOAT = args[1].downcast_ref::().unwrap(); + /// let x2: &FLOAT = args[2].downcast_ref::().unwrap(); + /// let y2: &FLOAT = args[3].downcast_ref::().unwrap(); + /// let square_sum = (y2 - y1).abs().powf(2.0) + (x2 -x1).abs().powf(2.0); + /// Ok(Dynamic::from(square_sum.sqrt())) + /// } + /// + /// fn clone_boxed(&self) -> Box { + /// Box::new(DistanceFunction()) /// } /// } - /// impl Plugin for AddOffsetPlugin { - /// fn name(&self) -> &str { - /// "My Plugin" - /// } + /// + /// // A simple custom plugin. This should not usually be done with hand-written code. + /// #[derive(Copy, Clone)] + /// pub struct AdvancedMathPlugin(); + /// + /// impl Plugin for AdvancedMathPlugin { /// fn register_contents(self, engine: &mut Engine) { - /// let add_offset_fn: Box Dynamic> = { - /// Box::new(move |x| self.add_offset(x)) - /// }; - /// engine.register_dynamic_fn("add_offset", add_offset_fn); + /// // Plugins are allowed to have side-effects on the engine. + /// engine.register_fn("get_mystic_number", || { 42 as FLOAT }); + /// + /// // Main purpose: create a module to expose the functions to Rhai. + /// // + /// // This is currently a hack. There needs to be a better API here for "plugin" + /// // modules. + /// let mut m = Module::new(); + /// m.set_fn("euclidean_distance".to_string(), FnAccess::Public, + /// &[std::any::TypeId::of::(), + /// std::any::TypeId::of::(), + /// std::any::TypeId::of::(), + /// std::any::TypeId::of::()], + /// CallableFunction::from_plugin(DistanceFunction())); + /// let mut r = StaticModuleResolver::new(); + /// r.insert("Math::Advanced".to_string(), m); + /// engine.set_module_resolver(Some(r)); /// } /// } /// + /// /// # fn main() -> Result<(), Box> { /// /// let mut engine = Engine::new(); - /// engine.register_plugin(AddOffsetPlugin(50)); + /// engine.register_plugin(AdvancedMathPlugin()); /// - /// assert_eq!(engine.eval::("add_offset(42)")?, 92); + /// assert_eq!(engine.eval::( + /// r#"import "Math::Advanced" as math; + /// let x = math::euclidean_distance(0.0, 1.0, 0.0, get_mystic_number()); x"#)?, 41.0); /// # Ok(()) /// # } /// ``` @@ -115,15 +152,6 @@ pub trait RegisterResultFn { fn register_result_fn(&mut self, name: &str, f: FN); } -/// Represents an externally-written plugin for the Rhai interpreter. -/// -/// This should not be used directly. Use the `plugin_module!` macro instead. -#[cfg(feature = "plugins")] -pub trait Plugin { - fn register_contents(self, engine: &mut Engine); - fn name(&self) -> &str; -} - // These types are used to build a unique _marker_ tuple type for each combination // of function parameter types in order to make each trait implementation unique. // That is because stable Rust currently does not allow distinguishing implementations @@ -156,10 +184,9 @@ pub fn by_value(data: &mut Dynamic) -> T { mem::take(data).cast::() } -#[cfg(feature = "plugins")] impl RegisterPlugin for Engine { fn register_plugin(&mut self, plugin: PL) { - plugin.register_contents(self) + plugin.register_contents(self); } } diff --git a/src/lib.rs b/src/lib.rs index 39920c0f..e0f2245c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,10 +75,14 @@ mod engine; mod error; mod fn_call; mod fn_func; -mod fn_native; +pub mod fn_native; mod fn_register; mod module; mod optimize; +#[cfg(not(feature = "no_module"))] +pub mod plugin; +#[cfg(feature = "no_module")] +mod plugin; pub mod packages; mod parser; mod result; @@ -91,9 +95,8 @@ mod utils; pub use any::Dynamic; pub use engine::Engine; pub use error::{ParseError, ParseErrorType}; -#[cfg(feature = "plugins")] -pub use fn_register::{Plugin, RegisterPlugin}; pub use fn_register::{RegisterFn, RegisterResultFn}; +pub use fn_register::RegisterPlugin; pub use module::Module; pub use parser::{ImmutableString, AST, INT}; pub use result::EvalAltResult; diff --git a/src/plugin.rs b/src/plugin.rs new file mode 100644 index 00000000..8159d3c8 --- /dev/null +++ b/src/plugin.rs @@ -0,0 +1,42 @@ +//! Module defining plugins in Rhai. Is exported for use by plugin authors. + +pub use crate::Engine; +pub use crate::any::{Dynamic, Variant}; +pub use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn}; +pub use crate::parser::{ + FnAccess, + FnAccess::{Private, Public}, + AST, +}; +pub use crate::result::EvalAltResult; +pub use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; +pub use crate::token::{Position, Token}; +pub use crate::utils::StaticVec; + +#[cfg(features = "sync")] +/// Represents an externally-written plugin for the Rhai interpreter. +/// +/// This trait should not be used directly. Use the `#[plugin]` procedural attribute instead. +pub trait Plugin: Send { + fn register_contents(self, engine: &mut Engine); +} + +#[cfg(not(features = "sync"))] +/// Represents an externally-written plugin for the Rhai interpreter. +/// +/// This trait should not be used directly. Use the `#[plugin]` procedural attribute instead. +pub trait Plugin: Send + Sync { + fn register_contents(self, engine: &mut Engine); +} + +/// Represents a function that is statically defined within a plugin. +/// +/// This trait should not be used directly. Use the `#[plugin]` procedural attribute instead. +pub trait PluginFunction { + fn is_method_call(&self) -> bool; + fn is_varadic(&self) -> bool; + + fn call(&self, args: &[&mut Dynamic], pos: Position) -> Result>; + + fn clone_boxed(&self) -> Box; +} From ec6e3daabbe688b582390cad120f3b15446fd944 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 3 Jun 2020 10:44:36 +0800 Subject: [PATCH 13/53] Refactor. --- src/api.rs | 40 ++++------------------ src/parser.rs | 92 +++++++++++++++++++++++++-------------------------- 2 files changed, 53 insertions(+), 79 deletions(-) diff --git a/src/api.rs b/src/api.rs index dc0a26d0..5a683d4a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -7,7 +7,7 @@ use crate::fn_call::FuncArgs; use crate::fn_native::{IteratorFn, SendSync}; use crate::fn_register::RegisterFn; use crate::optimize::{optimize_into_ast, OptimizationLevel}; -use crate::parser::{parse, parse_global_expr, AST}; +use crate::parser::AST; use crate::result::EvalAltResult; use crate::scope::Scope; use crate::token::{lex, Position}; @@ -449,14 +449,7 @@ impl Engine { optimization_level: OptimizationLevel, ) -> Result { let stream = lex(scripts); - - parse( - &mut stream.peekable(), - self, - scope, - optimization_level, - (self.max_expr_depth, self.max_function_expr_depth), - ) + self.parse(&mut stream.peekable(), scope, optimization_level) } /// Read the contents of a file into a string. @@ -578,13 +571,8 @@ impl Engine { // Trims the JSON string and add a '#' in front let scripts = ["#", json.trim()]; let stream = lex(&scripts); - let ast = parse_global_expr( - &mut stream.peekable(), - self, - &scope, - OptimizationLevel::None, - self.max_expr_depth, - )?; + let ast = + self.parse_global_expr(&mut stream.peekable(), &scope, OptimizationLevel::None)?; // Handle null - map to () if has_null { @@ -667,13 +655,7 @@ impl Engine { { let mut peekable = stream.peekable(); - parse_global_expr( - &mut peekable, - self, - scope, - self.optimization_level, - self.max_expr_depth, - ) + self.parse_global_expr(&mut peekable, scope, self.optimization_level) } } @@ -825,12 +807,10 @@ impl Engine { let scripts = [script]; let stream = lex(&scripts); - let ast = parse_global_expr( + let ast = self.parse_global_expr( &mut stream.peekable(), - self, scope, OptimizationLevel::None, // No need to optimize a lone expression - self.max_expr_depth, )?; self.eval_ast_with_scope(scope, &ast) @@ -957,13 +937,7 @@ impl Engine { let scripts = [script]; let stream = lex(&scripts); - let ast = parse( - &mut stream.peekable(), - self, - scope, - self.optimization_level, - (self.max_expr_depth, self.max_function_expr_depth), - )?; + let ast = self.parse(&mut stream.peekable(), scope, self.optimization_level)?; self.consume_ast_with_scope(scope, &ast) } diff --git a/src/parser.rs b/src/parser.rs index 5ff5e692..b2c4c1cf 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2478,44 +2478,15 @@ fn parse_fn<'a>( }) } -pub fn parse_global_expr<'a>( - input: &mut Peekable>, - engine: &Engine, - scope: &Scope, - optimization_level: OptimizationLevel, - max_expr_depth: usize, -) -> Result { - let mut state = ParseState::new(max_expr_depth); - let expr = parse_expr(input, &mut state, 0, false, false)?; - - match input.peek().unwrap() { - (Token::EOF, _) => (), - // Return error if the expression doesn't end - (token, pos) => { - return Err(PERR::BadInput(format!("Unexpected '{}'", token.syntax())).into_err(*pos)) - } - } - - Ok( - // Optimize AST - optimize_into_ast( - engine, - scope, - vec![Stmt::Expr(Box::new(expr))], - vec![], - optimization_level, - ), - ) -} - /// Parse the global level statements. fn parse_global_level<'a>( input: &mut Peekable>, - max_expr_depth: (usize, usize), + max_expr_depth: usize, + max_function_expr_depth: usize, ) -> Result<(Vec, Vec), ParseError> { let mut statements = Vec::::new(); let mut functions = HashMap::::with_hasher(StraightHasherBuilder); - let mut state = ParseState::new(max_expr_depth.0); + let mut state = ParseState::new(max_expr_depth); while !input.peek().unwrap().0.is_eof() { // Collect all the function definitions @@ -2530,7 +2501,7 @@ fn parse_global_level<'a>( match input.peek().unwrap() { #[cfg(not(feature = "no_function"))] (Token::Fn, _) => { - let mut state = ParseState::new(max_expr_depth.1); + let mut state = ParseState::new(max_function_expr_depth); let func = parse_fn(input, &mut state, access, 0, true, true)?; // Qualifiers (none) + function name + number of arguments. @@ -2586,20 +2557,49 @@ fn parse_global_level<'a>( Ok((statements, functions.into_iter().map(|(_, v)| v).collect())) } -/// Run the parser on an input stream, returning an AST. -pub fn parse<'a>( - input: &mut Peekable>, - engine: &Engine, - scope: &Scope, - optimization_level: OptimizationLevel, - max_expr_depth: (usize, usize), -) -> Result { - let (statements, lib) = parse_global_level(input, max_expr_depth)?; +impl Engine { + pub(crate) fn parse_global_expr<'a>( + &self, + input: &mut Peekable>, + scope: &Scope, + optimization_level: OptimizationLevel, + ) -> Result { + let mut state = ParseState::new(self.max_expr_depth); + let expr = parse_expr(input, &mut state, 0, false, false)?; - Ok( - // Optimize AST - optimize_into_ast(engine, scope, statements, lib, optimization_level), - ) + match input.peek().unwrap() { + (Token::EOF, _) => (), + // Return error if the expression doesn't end + (token, pos) => { + return Err( + PERR::BadInput(format!("Unexpected '{}'", token.syntax())).into_err(*pos) + ) + } + } + + let expr = vec![Stmt::Expr(Box::new(expr))]; + + Ok( + // Optimize AST + optimize_into_ast(self, scope, expr, Default::default(), optimization_level), + ) + } + + /// Run the parser on an input stream, returning an AST. + pub(crate) fn parse<'a>( + &self, + input: &mut Peekable>, + scope: &Scope, + optimization_level: OptimizationLevel, + ) -> Result { + let (statements, lib) = + parse_global_level(input, self.max_expr_depth, self.max_function_expr_depth)?; + + Ok( + // Optimize AST + optimize_into_ast(self, scope, statements, lib, optimization_level), + ) + } } /// Map a `Dynamic` value to an expression. From e21d25a0c57bb0b0d5dce6e31a2a22e297ba550a Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 3 Jun 2020 11:13:19 +0800 Subject: [PATCH 14/53] Add section on printing custom types. --- README.md | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1333579b..7021b924 100644 --- a/README.md +++ b/README.md @@ -472,8 +472,7 @@ The follow packages are available: Packages typically contain Rust functions that are callable within a Rhai script. All functions registered in a package is loaded under the _global namespace_ (i.e. they're available without module qualifiers). Once a package is created (e.g. via `new`), it can be _shared_ (via `get`) among multiple instances of [`Engine`], -even across threads (if the [`sync`] feature is turned on). -Therefore, a package only has to be created _once_. +even across threads (under the [`sync`] feature). Therefore, a package only has to be created _once_. Packages are actually implemented as [modules], so they share a lot of behavior and characteristics. The main difference is that a package loads under the _global_ namespace, while a module loads under its own @@ -573,7 +572,7 @@ if type_of(x) == "string" { [`Dynamic`]: #dynamic-values -A `Dynamic` value can be _any_ type. However, if the [`sync`] feature is used, then all types must be `Send + Sync`. +A `Dynamic` value can be _any_ type. However, under the [`sync`] feature, all types must be `Send + Sync`. Because [`type_of()`] a `Dynamic` value returns the type of the actual value, it is usually used to perform type-specific actions based on the actual value's type. @@ -976,7 +975,7 @@ let result = engine.eval::( println!("result: {}", result); // prints 1 ``` -If the [`no_object`] feature is turned on, however, the _method_ style of function calls +Under the [`no_object`] feature, however, the _method_ style of function calls (i.e. calling a function as an object-method) is no longer supported. ```rust @@ -1077,8 +1076,23 @@ println!("Answer: {}", result); // prints 42 ``` Needless to say, `register_type`, `register_type_with_name`, `register_get`, `register_set`, `register_get_set` -and `register_indexer` are not available when the [`no_object`] feature is turned on. -`register_indexer` is also not available when the [`no_index`] feature is turned on. +and `register_indexer` are not available under the [`no_object`] feature. +`register_indexer` is also not available under the [`no_index`] feature. + +Printing for custom types +------------------------- + +To use custom types for `print` and `debug`, or format its value into a [string], it is necessary that the following +functions be registered (assuming the custom type is `T` and it is `Display + Debug`): + +| Function | Signature | Typical implementation | Usage | +| ----------- | ------------------------------------------------ | ------------------------------ | --------------------------------------------------------------------------------------- | +| `to_string` | `|s: &mut T| -> String` | `s.to_string()` | Converts the custom type into a [string] | +| `print` | `|s: &mut T| -> String` | `s.to_string()` | Converts the custom type into a [string] for the [`print`](#print-and-debug) statement | +| `debug` | `|s: &mut T| -> String` | `format!("{:?}", s)` | Converts the custom type into a [string] for the [`debug`](#print-and-debug) statement | +| `+` | `|s1: ImmutableString, s: T| -> ImmutableString` | `s1 + s` | Append the custom type to another [string], for `print("Answer: " + type);` usage | +| `+` | `|s: T, s2: ImmutableString| -> String` | `s.to_string().push_str(&s2);` | Append another [string] to the custom type, for `print(type + " is the answer");` usage | +| `+=` | `|s1: &mut ImmutableString, s: T|` | `s1 += s.to_string()` | Append the custom type to an existing [string], for `s += type;` usage | `Scope` - Initializing and maintaining state ------------------------------------------- @@ -1089,8 +1103,8 @@ By default, Rhai treats each [`Engine`] invocation as a fresh one, persisting on but no global state. This gives each evaluation a clean starting slate. In order to continue using the same global state from one invocation to the next, such a state must be manually created and passed in. -All `Scope` variables are [`Dynamic`], meaning they can store values of any type. If the [`sync`] feature is used, however, -then only types that are `Send + Sync` are supported, and the entire `Scope` itself will also be `Send + Sync`. +All `Scope` variables are [`Dynamic`], meaning they can store values of any type. Under the [`sync`] feature, however, +only types that are `Send + Sync` are supported, and the entire `Scope` itself will also be `Send + Sync`. This is extremely useful in multi-threaded applications. In this example, a global state object (a `Scope`) is created with a few initialized variables, then the same state is @@ -1191,7 +1205,7 @@ The following are reserved keywords in Rhai: | `import`, `export`, `as` | Modules | [`no_module`] | Keywords cannot be the name of a [function] or [variable], unless the relevant exclusive feature is enabled. -For example, `fn` is a valid variable name if the [`no_function`] feature is used. +For example, `fn` is a valid variable name under the [`no_function`] feature. Statements ---------- @@ -1729,7 +1743,7 @@ technically be mapped to [`()`]. A valid JSON string does not start with a hash Rhai object map does - that's the major difference! JSON numbers are all floating-point while Rhai supports integers (`INT`) and floating-point (`FLOAT`) if -the [`no_float`] feature is not turned on. Most common generators of JSON data distinguish between +the [`no_float`] feature is not enabled. Most common generators of JSON data distinguish between integer and floating-point values by always serializing a floating-point number with a decimal point (i.e. `123.0` instead of `123` which is assumed to be an integer). This style can be used successfully with Rhai object maps. @@ -2364,7 +2378,7 @@ Built-in module resolvers are grouped under the `rhai::module_resolvers` module | Module Resolver | Description | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `FileModuleResolver` | The default module resolution service, not available under the [`no_std`] feature. Loads a script file (based off the current directory) with `.rhai` extension.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | -| `StaticModuleResolver` | Loads modules that are statically added. This can be used when the [`no_std`] feature is turned on. | +| `StaticModuleResolver` | Loads modules that are statically added. This can be used under the [`no_std`] feature. | An [`Engine`]'s module resolver is set via a call to `Engine::set_module_resolver`: From 56eb659d5a432e972b5668f107d1b10a7e8504eb Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 5 Jun 2020 15:14:42 +0800 Subject: [PATCH 15/53] Unify all functions into CallableFunction type, remove FunctionsLib. --- src/api.rs | 16 +-- src/engine.rs | 262 +++++++++++++++++++---------------------------- src/fn_native.rs | 48 ++++++++- src/module.rs | 115 ++++++++++++++------- src/optimize.rs | 103 ++++++++++++------- src/parser.rs | 25 +++-- 6 files changed, 316 insertions(+), 253 deletions(-) diff --git a/src/api.rs b/src/api.rs index 5a683d4a..6c7a6e7e 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,7 +1,9 @@ //! Module that defines the extern API of `Engine`. use crate::any::{Dynamic, Variant}; -use crate::engine::{make_getter, make_setter, Engine, State, FUNC_INDEXER}; +use crate::engine::{ + get_script_function_by_signature, make_getter, make_setter, Engine, State, FUNC_INDEXER, +}; use crate::error::ParseError; use crate::fn_call::FuncArgs; use crate::fn_native::{IteratorFn, SendSync}; @@ -1077,10 +1079,7 @@ impl Engine { arg_values: &mut [Dynamic], ) -> Result> { let mut args: StaticVec<_> = arg_values.iter_mut().collect(); - let lib = ast.lib(); - - let fn_def = lib - .get_function_by_signature(name, args.len(), true) + let fn_def = get_script_function_by_signature(ast.lib(), name, args.len(), true) .ok_or_else(|| { Box::new(EvalAltResult::ErrorFunctionNotFound( name.into(), @@ -1091,7 +1090,7 @@ impl Engine { let mut state = State::new(); let args = args.as_mut(); - self.call_script_fn(scope, &mut state, &lib, name, fn_def, args, 0) + self.call_script_fn(scope, &mut state, ast.lib(), name, fn_def, args, 0) } /// Optimize the `AST` with constants defined in an external Scope. @@ -1114,8 +1113,9 @@ impl Engine { ) -> AST { let lib = ast .lib() - .iter() - .map(|(_, fn_def)| fn_def.as_ref().clone()) + .iter_fn() + .filter(|(_, _, _, f)| f.is_script()) + .map(|(_, _, _, f)| f.get_fn_def().clone()) .collect(); let stmt = mem::take(ast.statements_mut()); diff --git a/src/engine.rs b/src/engine.rs index a5001df3..b79c497b 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -3,16 +3,16 @@ use crate::any::{Dynamic, Union}; use crate::calc_fn_hash; use crate::error::ParseErrorType; -use crate::fn_native::{CallableFunction, Callback, FnCallArgs, Shared}; +use crate::fn_native::{CallableFunction, Callback, FnCallArgs}; use crate::module::{resolvers, Module, ModuleResolver}; use crate::optimize::OptimizationLevel; use crate::packages::{CorePackage, Package, PackageLibrary, PackagesCollection, StandardPackage}; -use crate::parser::{Expr, FnAccess, FnDef, ImmutableString, ReturnType, Stmt, AST, INT}; +use crate::parser::{Expr, FnAccess, ImmutableString, ReturnType, ScriptFnDef, Stmt, AST, INT}; use crate::r#unsafe::{unsafe_cast_var_name_to_lifetime, unsafe_mut_cast_to_lifetime}; use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; use crate::token::Position; -use crate::utils::{StaticVec, StraightHasherBuilder}; +use crate::utils::StaticVec; #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; @@ -24,7 +24,6 @@ use crate::stdlib::{ format, iter::{empty, once}, mem, - ops::{Deref, DerefMut}, string::{String, ToString}, vec::Vec, }; @@ -189,88 +188,24 @@ impl State { } } -/// A type that holds a library (`HashMap`) of script-defined functions. -/// -/// Since script-defined functions have `Dynamic` parameters, functions with the same name -/// and number of parameters are considered equivalent. -/// -/// The key of the `HashMap` is a `u64` hash calculated by the function `calc_fn_hash`. -#[derive(Debug, Clone, Default)] -pub struct FunctionsLib(HashMap, StraightHasherBuilder>); +/// Get a script-defined function definition from a module. +pub fn get_script_function_by_signature<'a>( + module: &'a Module, + name: &str, + params: usize, + public_only: bool, +) -> Option<&'a ScriptFnDef> { + // Qualifiers (none) + function name + number of arguments. + let hash_fn_def = calc_fn_hash(empty(), name, params, empty()); + let func = module.get_fn(hash_fn_def)?; + if !func.is_script() { + return None; + } + let fn_def = func.get_fn_def(); -impl FunctionsLib { - /// Create a new `FunctionsLib` from a collection of `FnDef`. - pub fn from_iter(vec: impl IntoIterator) -> Self { - FunctionsLib( - vec.into_iter() - .map(|fn_def| { - // Qualifiers (none) + function name + number of arguments. - let hash = calc_fn_hash(empty(), &fn_def.name, fn_def.params.len(), empty()); - (hash, fn_def.into()) - }) - .collect(), - ) - } - /// Does a certain function exist in the `FunctionsLib`? - /// - /// The `u64` hash is calculated by the function `crate::calc_fn_hash`. - pub fn has_function(&self, hash_fn_def: u64) -> bool { - self.contains_key(&hash_fn_def) - } - /// Get a function definition from the `FunctionsLib`. - /// - /// The `u64` hash is calculated by the function `crate::calc_fn_hash`. - pub fn get_function(&self, hash_fn_def: u64) -> Option<&FnDef> { - self.get(&hash_fn_def).map(|fn_def| fn_def.as_ref()) - } - /// Get a function definition from the `FunctionsLib`. - pub fn get_function_by_signature( - &self, - name: &str, - params: usize, - public_only: bool, - ) -> Option<&FnDef> { - // Qualifiers (none) + function name + number of arguments. - let hash_fn_def = calc_fn_hash(empty(), name, params, empty()); - let fn_def = self.get_function(hash_fn_def); - - match fn_def.as_ref().map(|f| f.access) { - None => None, - Some(FnAccess::Private) if public_only => None, - Some(FnAccess::Private) | Some(FnAccess::Public) => fn_def, - } - } - /// Merge another `FunctionsLib` into this `FunctionsLib`. - pub fn merge(&self, other: &Self) -> Self { - if self.is_empty() { - other.clone() - } else if other.is_empty() { - self.clone() - } else { - let mut functions = self.clone(); - functions.extend(other.iter().map(|(hash, fn_def)| (*hash, fn_def.clone()))); - functions - } - } -} - -impl From)>> for FunctionsLib { - fn from(values: Vec<(u64, Shared)>) -> Self { - FunctionsLib(values.into_iter().collect()) - } -} - -impl Deref for FunctionsLib { - type Target = HashMap, StraightHasherBuilder>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for FunctionsLib { - fn deref_mut(&mut self) -> &mut HashMap, StraightHasherBuilder> { - &mut self.0 + match fn_def.access { + FnAccess::Private if public_only => None, + FnAccess::Private | FnAccess::Public => Some(&fn_def), } } @@ -575,7 +510,7 @@ impl Engine { &self, scope: &mut Scope, state: &mut State, - lib: &FunctionsLib, + lib: &Module, fn_name: &str, hashes: (u64, u64), args: &mut FnCallArgs, @@ -634,12 +569,27 @@ impl Engine { } } + // Search for the function // First search in script-defined functions (can override built-in) - if !native_only { - if let Some(fn_def) = lib.get(&hashes.1) { + // Then search registered native functions (can override packages) + // Then search packages + // NOTE: We skip script functions for global_module and packages, and native functions for lib + let func = if !native_only { + lib.get_fn(hashes.1) //.or_else(|| lib.get_fn(hashes.0)) + } else { + None + } + //.or_else(|| self.global_module.get_fn(hashes.1)) + .or_else(|| self.global_module.get_fn(hashes.0)) + //.or_else(|| self.packages.get_fn(hashes.1)) + .or_else(|| self.packages.get_fn(hashes.0)); + + if let Some(func) = func { + if func.is_script() { + // Run scripted function normalize_first_arg(is_ref, &mut this_copy, &mut old_this_ptr, args); - // Run scripted function + let fn_def = func.get_fn_def(); let result = self.call_script_fn(scope, state, lib, fn_name, fn_def, args, level)?; @@ -647,55 +597,48 @@ impl Engine { restore_first_arg(old_this_ptr, args); return Ok((result, false)); + } else { + // Calling pure function in method-call? + normalize_first_arg( + func.is_pure() && is_ref, + &mut this_copy, + &mut old_this_ptr, + args, + ); + + // Run external function + let result = func.get_native_fn()(args)?; + + // Restore the original reference + restore_first_arg(old_this_ptr, args); + + // See if the function match print/debug (which requires special processing) + return Ok(match fn_name { + KEYWORD_PRINT => ( + (self.print)(result.as_str().map_err(|type_name| { + Box::new(EvalAltResult::ErrorMismatchOutputType( + type_name.into(), + Position::none(), + )) + })?) + .into(), + false, + ), + KEYWORD_DEBUG => ( + (self.debug)(result.as_str().map_err(|type_name| { + Box::new(EvalAltResult::ErrorMismatchOutputType( + type_name.into(), + Position::none(), + )) + })?) + .into(), + false, + ), + _ => (result, func.is_method()), + }); } } - // Search built-in's and external functions - if let Some(func) = self - .global_module - .get_fn(hashes.0) - .or_else(|| self.packages.get_fn(hashes.0)) - { - // Calling pure function in method-call? - normalize_first_arg( - func.is_pure() && is_ref, - &mut this_copy, - &mut old_this_ptr, - args, - ); - - // Run external function - let result = func.get_native_fn()(args)?; - - // Restore the original reference - restore_first_arg(old_this_ptr, args); - - // See if the function match print/debug (which requires special processing) - return Ok(match fn_name { - KEYWORD_PRINT => ( - (self.print)(result.as_str().map_err(|type_name| { - Box::new(EvalAltResult::ErrorMismatchOutputType( - type_name.into(), - Position::none(), - )) - })?) - .into(), - false, - ), - KEYWORD_DEBUG => ( - (self.debug)(result.as_str().map_err(|type_name| { - Box::new(EvalAltResult::ErrorMismatchOutputType( - type_name.into(), - Position::none(), - )) - })?) - .into(), - false, - ), - _ => (result, func.is_method()), - }); - } - // See if it is built in. if args.len() == 2 { match run_builtin_binary_op(fn_name, args[0], args[1])? { @@ -757,9 +700,9 @@ impl Engine { &self, scope: &mut Scope, state: &mut State, - lib: &FunctionsLib, + lib: &Module, fn_name: &str, - fn_def: &FnDef, + fn_def: &ScriptFnDef, args: &mut FnCallArgs, level: usize, ) -> Result> { @@ -809,13 +752,18 @@ impl Engine { } // Has a system function an override? - fn has_override(&self, lib: &FunctionsLib, hashes: (u64, u64)) -> bool { - // First check registered functions - self.global_module.contains_fn(hashes.0) - // Then check packages - || self.packages.contains_fn(hashes.0) - // Then check script-defined functions - || lib.contains_key(&hashes.1) + fn has_override(&self, lib: &Module, hashes: (u64, u64)) -> bool { + // NOTE: We skip script functions for global_module and packages, and native functions for lib + + // First check script-defined functions + lib.contains_fn(hashes.1) + //|| lib.contains_fn(hashes.0) + // Then check registered functions + //|| self.global_module.contains_fn(hashes.1) + || self.global_module.contains_fn(hashes.0) + // Then check packages + //|| self.packages.contains_fn(hashes.1) + || self.packages.contains_fn(hashes.0) } /// Perform an actual function call, taking care of special functions @@ -829,7 +777,7 @@ impl Engine { fn exec_fn_call( &self, state: &mut State, - lib: &FunctionsLib, + lib: &Module, fn_name: &str, native_only: bool, hash_fn_def: u64, @@ -878,7 +826,7 @@ impl Engine { &self, scope: &mut Scope, state: &mut State, - lib: &FunctionsLib, + lib: &Module, script: &Dynamic, ) -> Result> { let script = script.as_str().map_err(|type_name| { @@ -894,7 +842,7 @@ impl Engine { )?; // If new functions are defined within the eval string, it is an error - if !ast.lib().is_empty() { + if ast.lib().num_fn() != 0 { return Err(Box::new(EvalAltResult::ErrorParsing( ParseErrorType::WrongFnDefinition.into_err(Position::none()), ))); @@ -917,7 +865,7 @@ impl Engine { fn eval_dot_index_chain_helper( &self, state: &mut State, - lib: &FunctionsLib, + lib: &Module, target: &mut Target, rhs: &Expr, idx_values: &mut StaticVec, @@ -1097,7 +1045,7 @@ impl Engine { &self, scope: &mut Scope, state: &mut State, - lib: &FunctionsLib, + lib: &Module, expr: &Expr, level: usize, new_val: Option, @@ -1166,7 +1114,7 @@ impl Engine { &self, scope: &mut Scope, state: &mut State, - lib: &FunctionsLib, + lib: &Module, expr: &Expr, idx_values: &mut StaticVec, size: usize, @@ -1211,7 +1159,7 @@ impl Engine { fn get_indexed_mut<'a>( &self, state: &mut State, - lib: &FunctionsLib, + lib: &Module, val: &'a mut Dynamic, is_ref: bool, mut idx: Dynamic, @@ -1305,7 +1253,7 @@ impl Engine { &self, scope: &mut Scope, state: &mut State, - lib: &FunctionsLib, + lib: &Module, lhs: &Expr, rhs: &Expr, level: usize, @@ -1368,7 +1316,7 @@ impl Engine { &self, scope: &mut Scope, state: &mut State, - lib: &FunctionsLib, + lib: &Module, expr: &Expr, level: usize, ) -> Result> { @@ -1650,14 +1598,14 @@ impl Engine { }; match func { - Ok(x) if x.is_script() => { + Ok(f) if f.is_script() => { let args = args.as_mut(); - let fn_def = x.get_fn_def(); + let fn_def = f.get_fn_def(); let mut scope = Scope::new(); self.call_script_fn(&mut scope, state, lib, name, fn_def, args, level) .map_err(|err| EvalAltResult::new_position(err, *pos)) } - Ok(x) => x.get_native_fn()(args.as_mut()).map_err(|err| err.new_position(*pos)), + Ok(f) => f.get_native_fn()(args.as_mut()).map_err(|err| err.new_position(*pos)), Err(err) if def_val.is_some() && matches!(*err, EvalAltResult::ErrorFunctionNotFound(_, _)) => @@ -1719,7 +1667,7 @@ impl Engine { &self, scope: &mut Scope, state: &mut State, - lib: &FunctionsLib, + lib: &Module, stmt: &Stmt, level: usize, ) -> Result> { diff --git a/src/fn_native.rs b/src/fn_native.rs index 334068b5..bf6219e1 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -1,8 +1,8 @@ use crate::any::Dynamic; -use crate::parser::FnDef; +use crate::parser::ScriptFnDef; use crate::result::EvalAltResult; -use crate::stdlib::{boxed::Box, rc::Rc, sync::Arc}; +use crate::stdlib::{boxed::Box, fmt, rc::Rc, sync::Arc}; #[cfg(feature = "sync")] pub trait SendSync: Send + Sync {} @@ -73,7 +73,18 @@ pub enum CallableFunction { /// An iterator function. Iterator(IteratorFn), /// A script-defined function. - Script(Shared), + Script(Shared), +} + +impl fmt::Debug for CallableFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Pure(_) => write!(f, "NativePureFunction"), + Self::Method(_) => write!(f, "NativeMethod"), + Self::Iterator(_) => write!(f, "NativeIterator"), + Self::Script(fn_def) => write!(f, "{:?}", fn_def), + } + } } impl CallableFunction { @@ -116,12 +127,23 @@ impl CallableFunction { Self::Iterator(_) | Self::Script(_) => panic!(), } } + /// Get a shared reference to a script-defined function definition. + /// + /// # Panics + /// + /// Panics if the `CallableFunction` is not `Script`. + pub fn get_shared_fn_def(&self) -> Shared { + match self { + Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => panic!(), + Self::Script(f) => f.clone(), + } + } /// Get a reference to a script-defined function definition. /// /// # Panics /// /// Panics if the `CallableFunction` is not `Script`. - pub fn get_fn_def(&self) -> &FnDef { + pub fn get_fn_def(&self) -> &ScriptFnDef { match self { Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => panic!(), Self::Script(f) => f, @@ -147,3 +169,21 @@ impl CallableFunction { Self::Method(func.into()) } } + +impl From for CallableFunction { + fn from(func: IteratorFn) -> Self { + Self::Iterator(func) + } +} + +impl From for CallableFunction { + fn from(func: ScriptFnDef) -> Self { + Self::Script(func.into()) + } +} + +impl From> for CallableFunction { + fn from(func: Shared) -> Self { + Self::Script(func) + } +} diff --git a/src/module.rs b/src/module.rs index cd567a57..d509f3c1 100644 --- a/src/module.rs +++ b/src/module.rs @@ -2,12 +2,12 @@ use crate::any::{Dynamic, Variant}; use crate::calc_fn_hash; -use crate::engine::{make_getter, make_setter, Engine, FunctionsLib, FUNC_INDEXER}; +use crate::engine::{make_getter, make_setter, Engine, FUNC_INDEXER}; use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync}; use crate::parser::{ FnAccess, FnAccess::{Private, Public}, - AST, + ScriptFnDef, AST, }; use crate::result::EvalAltResult; use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; @@ -53,9 +53,6 @@ pub struct Module { StraightHasherBuilder, >, - /// Script-defined functions. - lib: FunctionsLib, - /// Iterator functions, keyed by the type producing the iterator. type_iterators: HashMap, @@ -68,10 +65,9 @@ impl fmt::Debug for Module { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "", + "", self.variables, self.functions.len(), - self.lib.len() ) } } @@ -186,6 +182,23 @@ impl Module { .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.to_string(), pos))) } + /// Set a script-defined function into the module. + /// + /// If there is an existing function of the same name and number of arguments, it is replaced. + pub(crate) fn set_script_fn(&mut self, fn_def: ScriptFnDef) { + // None + function name + number of arguments. + let hash_fn_def = calc_fn_hash(empty(), &fn_def.name, fn_def.params.len(), empty()); + self.functions.insert( + hash_fn_def, + ( + fn_def.name.to_string(), + fn_def.access, + Default::default(), + fn_def.into(), + ), + ); + } + /// Does a sub-module exist in the module? /// /// # Examples @@ -750,6 +763,41 @@ impl Module { }) } + /// Merge another module into this module. + pub fn merge(&mut self, other: &Self) { + self.variables + .extend(other.variables.iter().map(|(k, v)| (k.clone(), v.clone()))); + self.functions + .extend(other.functions.iter().map(|(&k, v)| (k, v.clone()))); + self.type_iterators + .extend(other.type_iterators.iter().map(|(&k, v)| (k, v.clone()))); + } + + /// Get the number of variables in the module. + pub fn num_var(&self) -> usize { + self.variables.len() + } + /// Get the number of functions in the module. + pub fn num_fn(&self) -> usize { + self.variables.len() + } + /// Get the number of type iterators in the module. + pub fn num_iter(&self) -> usize { + self.variables.len() + } + + /// Get an iterator to the variables in the module. + pub fn iter_var(&self) -> impl Iterator { + self.variables.iter() + } + + /// Get an iterator to the functions in the module. + pub(crate) fn iter_fn( + &self, + ) -> impl Iterator, CallableFunction)> { + self.functions.values() + } + /// Create a new `Module` by evaluating an `AST`. /// /// # Examples @@ -795,12 +843,12 @@ impl Module { }, ); - module.lib = module.lib.merge(ast.lib()); + module.merge(ast.lib()); Ok(module) } - /// Scan through all the sub-modules in the `Module` build an index of all + /// Scan through all the sub-modules in the module build an index of all /// variables and external Rust functions via hashing. pub(crate) fn index_all_sub_modules(&mut self) { // Collect a particular module. @@ -830,34 +878,31 @@ impl Module { Private => continue, Public => (), } - // Rust functions are indexed in two steps: - // 1) Calculate a hash in a similar manner to script-defined functions, - // i.e. qualifiers + function name + number of arguments. - let hash_fn_def = - calc_fn_hash(qualifiers.iter().map(|&v| v), name, params.len(), empty()); - // 2) Calculate a second hash with no qualifiers, empty function name, - // zero number of arguments, and the actual list of argument `TypeId`'.s - let hash_fn_args = calc_fn_hash(empty(), "", 0, params.iter().cloned()); - // 3) The final hash is the XOR of the two hashes. - let hash_fn_native = hash_fn_def ^ hash_fn_args; - functions.push((hash_fn_native, func.clone())); - } - // Index all script-defined functions - for fn_def in module.lib.values() { - match fn_def.access { - // Private functions are not exported - Private => continue, - Public => (), + if func.is_script() { + let fn_def = func.get_shared_fn_def(); + // Qualifiers + function name + number of arguments. + let hash_fn_def = calc_fn_hash( + qualifiers.iter().map(|&v| v), + &fn_def.name, + fn_def.params.len(), + empty(), + ); + functions.push((hash_fn_def, fn_def.into())); + } else { + // Rust functions are indexed in two steps: + // 1) Calculate a hash in a similar manner to script-defined functions, + // i.e. qualifiers + function name + number of arguments. + let hash_fn_def = + calc_fn_hash(qualifiers.iter().map(|&v| v), name, params.len(), empty()); + // 2) Calculate a second hash with no qualifiers, empty function name, + // zero number of arguments, and the actual list of argument `TypeId`'.s + let hash_fn_args = calc_fn_hash(empty(), "", 0, params.iter().cloned()); + // 3) The final hash is the XOR of the two hashes. + let hash_fn_native = hash_fn_def ^ hash_fn_args; + + functions.push((hash_fn_native, func.clone())); } - // Qualifiers + function name + number of arguments. - let hash_fn_def = calc_fn_hash( - qualifiers.iter().map(|&v| v), - &fn_def.name, - fn_def.params.len(), - empty(), - ); - functions.push((hash_fn_def, CallableFunction::Script(fn_def.clone()).into())); } } diff --git a/src/optimize.rs b/src/optimize.rs index ea27620d..7895fc68 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -1,10 +1,8 @@ use crate::any::Dynamic; use crate::calc_fn_hash; -use crate::engine::{ - Engine, FunctionsLib, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_PRINT, - KEYWORD_TYPE_OF, -}; -use crate::parser::{map_dynamic_to_expr, Expr, FnDef, ReturnType, Stmt, AST}; +use crate::engine::{Engine, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_PRINT, KEYWORD_TYPE_OF}; +use crate::module::Module; +use crate::parser::{map_dynamic_to_expr, Expr, ReturnType, ScriptFnDef, Stmt, AST}; use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; use crate::utils::StaticVec; @@ -54,14 +52,14 @@ struct State<'a> { /// An `Engine` instance for eager function evaluation. engine: &'a Engine, /// Library of script-defined functions. - lib: &'a FunctionsLib, + lib: &'a Module, /// Optimization level. optimization_level: OptimizationLevel, } impl<'a> State<'a> { /// Create a new State. - pub fn new(engine: &'a Engine, lib: &'a FunctionsLib, level: OptimizationLevel) -> Self { + pub fn new(engine: &'a Engine, lib: &'a Module, level: OptimizationLevel) -> Self { Self { changed: false, constants: vec![], @@ -547,14 +545,15 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { && state.optimization_level == OptimizationLevel::Full // full optimizations && x.3.iter().all(|expr| expr.is_constant()) // all arguments are constants => { - let ((name, native_only, pos), _, _, args, def_value) = x.as_mut(); + let ((name, _, pos), _, _, args, def_value) = x.as_mut(); - // First search in script-defined functions (can override built-in) + // First search in functions lib (can override built-in) // Cater for both normal function call style and method call style (one additional arguments) - if !*native_only && state.lib.values().find(|f| - &f.name == name - && (args.len()..=args.len() + 1).contains(&f.params.len()) - ).is_some() { + if state.lib.iter_fn().find(|(_, _, _, f)| { + if !f.is_script() { return false; } + let fn_def = f.get_fn_def(); + &fn_def.name == name && (args.len()..=args.len() + 1).contains(&fn_def.params.len()) + }).is_some() { // A script-defined function overrides the built-in function - do not make the call x.3 = x.3.into_iter().map(|a| optimize_expr(a, state)).collect(); return Expr::FnCall(x); @@ -616,7 +615,7 @@ fn optimize( statements: Vec, engine: &Engine, scope: &Scope, - lib: &FunctionsLib, + lib: &Module, level: OptimizationLevel, ) -> Vec { // If optimization level is None then skip optimizing @@ -703,7 +702,7 @@ pub fn optimize_into_ast( engine: &Engine, scope: &Scope, statements: Vec, - functions: Vec, + functions: Vec, level: OptimizationLevel, ) -> AST { #[cfg(feature = "no_optimize")] @@ -711,37 +710,65 @@ pub fn optimize_into_ast( #[cfg(not(feature = "no_function"))] let lib = { + let mut module = Module::new(); + if !level.is_none() { - let lib = FunctionsLib::from_iter(functions.iter().cloned()); + // We only need the script library's signatures for optimization purposes + let mut lib2 = Module::new(); - FunctionsLib::from_iter(functions.into_iter().map(|mut fn_def| { - let pos = fn_def.body.position(); - - // Optimize the function body - let mut body = optimize(vec![fn_def.body], engine, &Scope::new(), &lib, level); - - // {} -> Noop - fn_def.body = match body.pop().unwrap_or_else(|| Stmt::Noop(pos)) { - // { return val; } -> val - Stmt::ReturnWithVal(x) if x.1.is_some() && (x.0).0 == ReturnType::Return => { - Stmt::Expr(Box::new(x.1.unwrap())) + functions + .iter() + .map(|fn_def| { + ScriptFnDef { + name: fn_def.name.clone(), + access: fn_def.access, + body: Default::default(), + params: fn_def.params.clone(), + pos: fn_def.pos, } - // { return; } -> () - Stmt::ReturnWithVal(x) if x.1.is_none() && (x.0).0 == ReturnType::Return => { - Stmt::Expr(Box::new(Expr::Unit((x.0).1))) - } - // All others - stmt => stmt, - }; - fn_def - })) + .into() + }) + .for_each(|fn_def| lib2.set_script_fn(fn_def)); + + functions + .into_iter() + .map(|mut fn_def| { + let pos = fn_def.body.position(); + + // Optimize the function body + let mut body = optimize(vec![fn_def.body], engine, &Scope::new(), &lib2, level); + + // {} -> Noop + fn_def.body = match body.pop().unwrap_or_else(|| Stmt::Noop(pos)) { + // { return val; } -> val + Stmt::ReturnWithVal(x) + if x.1.is_some() && (x.0).0 == ReturnType::Return => + { + Stmt::Expr(Box::new(x.1.unwrap())) + } + // { return; } -> () + Stmt::ReturnWithVal(x) + if x.1.is_none() && (x.0).0 == ReturnType::Return => + { + Stmt::Expr(Box::new(Expr::Unit((x.0).1))) + } + // All others + stmt => stmt, + }; + fn_def.into() + }) + .for_each(|fn_def| module.set_script_fn(fn_def)); } else { - FunctionsLib::from_iter(functions.into_iter()) + functions + .into_iter() + .for_each(|fn_def| module.set_script_fn(fn_def)); } + + module }; #[cfg(feature = "no_function")] - let lib: FunctionsLib = Default::default(); + let lib = Default::default(); AST::new( match level { diff --git a/src/parser.rs b/src/parser.rs index b2c4c1cf..ab8de95c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,9 +2,9 @@ use crate::any::{Dynamic, Union}; use crate::calc_fn_hash; -use crate::engine::{make_getter, make_setter, Engine, FunctionsLib}; +use crate::engine::{make_getter, make_setter, Engine}; use crate::error::{LexError, ParseError, ParseErrorType}; -use crate::module::ModuleRef; +use crate::module::{Module, ModuleRef}; use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::scope::{EntryType as ScopeEntryType, Scope}; use crate::token::{Position, Token, TokenIterator}; @@ -55,12 +55,12 @@ pub struct AST( /// Global statements. Vec, /// Script-defined functions. - FunctionsLib, + Module, ); impl AST { /// Create a new `AST`. - pub fn new(statements: Vec, lib: FunctionsLib) -> Self { + pub fn new(statements: Vec, lib: Module) -> Self { Self(statements, lib) } @@ -75,7 +75,7 @@ impl AST { } /// Get the script-defined functions. - pub(crate) fn lib(&self) -> &FunctionsLib { + pub(crate) fn lib(&self) -> &Module { &self.1 } @@ -135,7 +135,10 @@ impl AST { (true, true) => vec![], }; - Self::new(ast, functions.merge(&other.1)) + let mut functions = functions.clone(); + functions.merge(&other.1); + + Self::new(ast, functions) } /// Clear all function definitions in the `AST`. @@ -170,7 +173,7 @@ pub enum FnAccess { /// A scripted function definition. #[derive(Debug, Clone)] -pub struct FnDef { +pub struct ScriptFnDef { /// Function name. pub name: String, /// Function access mode. @@ -2393,7 +2396,7 @@ fn parse_fn<'a>( level: usize, if_expr: bool, stmt_expr: bool, -) -> Result { +) -> Result { let pos = eat_token(input, Token::Fn); if level > state.max_expr_depth { @@ -2469,7 +2472,7 @@ fn parse_fn<'a>( let params = params.into_iter().map(|(p, _)| p).collect(); - Ok(FnDef { + Ok(ScriptFnDef { name, access, params, @@ -2483,9 +2486,9 @@ fn parse_global_level<'a>( input: &mut Peekable>, max_expr_depth: usize, max_function_expr_depth: usize, -) -> Result<(Vec, Vec), ParseError> { +) -> Result<(Vec, Vec), ParseError> { let mut statements = Vec::::new(); - let mut functions = HashMap::::with_hasher(StraightHasherBuilder); + let mut functions = HashMap::::with_hasher(StraightHasherBuilder); let mut state = ParseState::new(max_expr_depth); while !input.peek().unwrap().0.is_eof() { From 5f40a1376aa594c33d3e2077e7079dc925de2ed7 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 6 Jun 2020 13:06:00 +0800 Subject: [PATCH 16/53] Implement index setters. --- README.md | 59 +++++++------ RELEASES.md | 7 ++ src/api.rs | 107 +++++++++++++++++++++-- src/engine.rs | 220 ++++++++++++++++++++++++++++++----------------- src/module.rs | 48 +++++++++-- tests/get_set.rs | 19 ++-- 6 files changed, 339 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index 7021b924..39539e54 100644 --- a/README.md +++ b/README.md @@ -472,7 +472,7 @@ The follow packages are available: Packages typically contain Rust functions that are callable within a Rhai script. All functions registered in a package is loaded under the _global namespace_ (i.e. they're available without module qualifiers). Once a package is created (e.g. via `new`), it can be _shared_ (via `get`) among multiple instances of [`Engine`], -even across threads (under the [`sync`] feature). Therefore, a package only has to be created _once_. +even across threads (under [`sync`]). Therefore, a package only has to be created _once_. Packages are actually implemented as [modules], so they share a lot of behavior and characteristics. The main difference is that a package loads under the _global_ namespace, while a module loads under its own @@ -572,7 +572,7 @@ if type_of(x) == "string" { [`Dynamic`]: #dynamic-values -A `Dynamic` value can be _any_ type. However, under the [`sync`] feature, all types must be `Send + Sync`. +A `Dynamic` value can be _any_ type. However, under [`sync`], all types must be `Send + Sync`. Because [`type_of()`] a `Dynamic` value returns the type of the actual value, it is usually used to perform type-specific actions based on the actual value's type. @@ -975,8 +975,8 @@ let result = engine.eval::( println!("result: {}", result); // prints 1 ``` -Under the [`no_object`] feature, however, the _method_ style of function calls -(i.e. calling a function as an object-method) is no longer supported. +Under [`no_object`], however, the _method_ style of function calls (i.e. calling a function as an object-method) +is no longer supported. ```rust // Below is a syntax error under 'no_object' because 'clear' cannot be called in method style. @@ -999,8 +999,7 @@ let x = new_ts(); print(x.type_of()); // prints "Hello" ``` -Getters and setters -------------------- +### Getters and setters Similarly, custom types can expose members by registering a `get` and/or `set` function. @@ -1040,12 +1039,10 @@ let result = engine.eval::(r#"let a = new_ts(); a.xyz = "42"; a.xyz"#)?; println!("Answer: {}", result); // prints 42 ``` -Indexers --------- +### Indexers Custom types can also expose an _indexer_ by registering an indexer function. A custom type with an indexer function defined can use the bracket '`[]`' notation to get a property value -(but not update it - indexers are read-only). ```rust #[derive(Clone)] @@ -1057,9 +1054,12 @@ impl TestStruct { fn get_field(&mut self, index: i64) -> i64 { self.fields[index as usize] } + fn set_field(&mut self, index: i64, value: i64) { + self.fields[index as usize] = value + } fn new() -> Self { - TestStruct { fields: vec![1, 2, 42, 4, 5] } + TestStruct { fields: vec![1, 2, 3, 4, 5] } } } @@ -1068,22 +1068,31 @@ let engine = Engine::new(); engine.register_type::(); engine.register_fn("new_ts", TestStruct::new); -engine.register_indexer(TestStruct::get_field); -let result = engine.eval::("let a = new_ts(); a[2]")?; +// Shorthand: engine.register_indexer_get_set(TestStruct::get_field, TestStruct::set_field); +engine.register_indexer_get(TestStruct::get_field); +engine.register_indexer_set(TestStruct::set_field); + +let result = engine.eval::("let a = new_ts(); a[2] = 42; a[2]")?; println!("Answer: {}", result); // prints 42 ``` -Needless to say, `register_type`, `register_type_with_name`, `register_get`, `register_set`, `register_get_set` -and `register_indexer` are not available under the [`no_object`] feature. -`register_indexer` is also not available under the [`no_index`] feature. +For efficiency reasons, indexers **cannot** be used to overload (i.e. override) built-in indexing operations for +[arrays] and [object maps]. -Printing for custom types -------------------------- +### Disabling custom types -To use custom types for `print` and `debug`, or format its value into a [string], it is necessary that the following -functions be registered (assuming the custom type is `T` and it is `Display + Debug`): +The custom types API `register_type`, `register_type_with_name`, `register_get`, `register_set`, `register_get_set`, +`register_indexer_get`, `register_indexer_set` and `register_indexer_get_set` are not available under [`no_object`]. + +The indexers API `register_indexer_get`, `register_indexer_set` and `register_indexer_get_set` are also +not available under [`no_index`]. + +### Printing for custom types + +To use custom types for `print` and `debug`, or convert its value into a [string], it is necessary that the following +functions be registered (assuming the custom type is `T : Display + Debug`): | Function | Signature | Typical implementation | Usage | | ----------- | ------------------------------------------------ | ------------------------------ | --------------------------------------------------------------------------------------- | @@ -1103,7 +1112,7 @@ By default, Rhai treats each [`Engine`] invocation as a fresh one, persisting on but no global state. This gives each evaluation a clean starting slate. In order to continue using the same global state from one invocation to the next, such a state must be manually created and passed in. -All `Scope` variables are [`Dynamic`], meaning they can store values of any type. Under the [`sync`] feature, however, +All `Scope` variables are [`Dynamic`], meaning they can store values of any type. Under [`sync`], however, only types that are `Send + Sync` are supported, and the entire `Scope` itself will also be `Send + Sync`. This is extremely useful in multi-threaded applications. @@ -1205,7 +1214,7 @@ The following are reserved keywords in Rhai: | `import`, `export`, `as` | Modules | [`no_module`] | Keywords cannot be the name of a [function] or [variable], unless the relevant exclusive feature is enabled. -For example, `fn` is a valid variable name under the [`no_function`] feature. +For example, `fn` is a valid variable name under [`no_function`]. Statements ---------- @@ -2375,10 +2384,10 @@ which simply loads a script file based on the path (with `.rhai` extension attac Built-in module resolvers are grouped under the `rhai::module_resolvers` module namespace. -| Module Resolver | Description | -| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `FileModuleResolver` | The default module resolution service, not available under the [`no_std`] feature. Loads a script file (based off the current directory) with `.rhai` extension.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | -| `StaticModuleResolver` | Loads modules that are statically added. This can be used under the [`no_std`] feature. | +| Module Resolver | Description | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `FileModuleResolver` | The default module resolution service, not available under [`no_std`]. Loads a script file (based off the current directory) with `.rhai` extension.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | +| `StaticModuleResolver` | Loads modules that are statically added. This can be used under [`no_std`]. | An [`Engine`]'s module resolver is set via a call to `Engine::set_module_resolver`: diff --git a/RELEASES.md b/RELEASES.md index 7ba44391..58303958 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -8,6 +8,13 @@ Breaking changes ---------------- * Callback closure passed to `Engine::on_progress` now takes `&u64` instead of `u64` to be consistent with other callback signatures. +* `Engine::register_indexer` is renamed to `Engine::register_indexer_get`. +* `Module::set_indexer_fn` is renamed to `Module::set_indexer_get_fn`. + +New features +------------ + +* Indexers are now split into getters ans setters (which now support updates). The API is split into `Engine::register_indexer_get` and `Engine::register_indexer_set` with `Engine::register_indexer_get_set` being a shorthand. Similarly, `Module::set_indexer_get_fn` and `Module::set_indexer_set_fn` are added. Version 0.15.0 diff --git a/src/api.rs b/src/api.rs index 6c7a6e7e..df92f891 100644 --- a/src/api.rs +++ b/src/api.rs @@ -2,7 +2,8 @@ use crate::any::{Dynamic, Variant}; use crate::engine::{ - get_script_function_by_signature, make_getter, make_setter, Engine, State, FUNC_INDEXER, + get_script_function_by_signature, make_getter, make_setter, Engine, State, FUNC_INDEXER_GET, + FUNC_INDEXER_SET, }; use crate::error::ParseError; use crate::fn_call::FuncArgs; @@ -275,7 +276,7 @@ impl Engine { self.register_set(name, set_fn); } - /// Register an indexer function for a registered type with the `Engine`. + /// Register an index getter for a registered type with the `Engine`. /// /// The function signature must start with `&mut self` and not `&self`. /// @@ -305,7 +306,7 @@ impl Engine { /// engine.register_fn("new_ts", TestStruct::new); /// /// // Register an indexer. - /// engine.register_indexer(TestStruct::get_field); + /// engine.register_indexer_get(TestStruct::get_field); /// /// assert_eq!(engine.eval::("let a = new_ts(); a[2]")?, 3); /// # Ok(()) @@ -313,7 +314,7 @@ impl Engine { /// ``` #[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_index"))] - pub fn register_indexer( + pub fn register_indexer_get( &mut self, callback: impl Fn(&mut T, X) -> U + SendSync + 'static, ) where @@ -321,7 +322,103 @@ impl Engine { U: Variant + Clone, X: Variant + Clone, { - self.register_fn(FUNC_INDEXER, callback); + self.register_fn(FUNC_INDEXER_GET, callback); + } + + /// Register an index setter for a registered type with the `Engine`. + /// + /// # Example + /// + /// ``` + /// #[derive(Clone)] + /// struct TestStruct { + /// fields: Vec + /// } + /// + /// impl TestStruct { + /// fn new() -> Self { TestStruct { fields: vec![1, 2, 3, 4, 5] } } + /// fn set_field(&mut self, index: i64, value: i64) { self.fields[index as usize] = value; } + /// } + /// + /// # fn main() -> Result<(), Box> { + /// use rhai::{Engine, RegisterFn}; + /// + /// let mut engine = Engine::new(); + /// + /// // Register the custom type. + /// engine.register_type::(); + /// + /// engine.register_fn("new_ts", TestStruct::new); + /// + /// // Register an indexer. + /// engine.register_indexer_set(TestStruct::set_field); + /// + /// assert_eq!( + /// engine.eval::("let a = new_ts(); a[2] = 42; a")?.fields[2], + /// 42 + /// ); + /// # Ok(()) + /// # } + /// ``` + #[cfg(not(feature = "no_object"))] + #[cfg(not(feature = "no_index"))] + pub fn register_indexer_set( + &mut self, + callback: impl Fn(&mut T, X, U) -> () + SendSync + 'static, + ) where + T: Variant + Clone, + U: Variant + Clone, + X: Variant + Clone, + { + self.register_fn(FUNC_INDEXER_SET, callback); + } + + /// Shorthand for register both index getter and setter functions for a registered type with the `Engine`. + /// + /// # Example + /// + /// ``` + /// #[derive(Clone)] + /// struct TestStruct { + /// fields: Vec + /// } + /// + /// impl TestStruct { + /// fn new() -> Self { TestStruct { fields: vec![1, 2, 3, 4, 5] } } + /// fn get_field(&mut self, index: i64) -> i64 { self.fields[index as usize] } + /// fn set_field(&mut self, index: i64, value: i64) { self.fields[index as usize] = value; } + /// } + /// + /// # fn main() -> Result<(), Box> { + /// use rhai::{Engine, RegisterFn}; + /// + /// let mut engine = Engine::new(); + /// + /// // Register the custom type. + /// engine.register_type::(); + /// + /// engine.register_fn("new_ts", TestStruct::new); + /// + /// // Register an indexer. + /// engine.register_indexer_get_set(TestStruct::get_field, TestStruct::set_field); + /// + /// assert_eq!(engine.eval::("let a = new_ts(); a[2] = 42; a[2]")?, 42); + /// # Ok(()) + /// # } + /// ``` + #[cfg(not(feature = "no_object"))] + #[cfg(not(feature = "no_index"))] + pub fn register_indexer_get_set( + &mut self, + getter: impl Fn(&mut T, X) -> U + SendSync + 'static, + setter: impl Fn(&mut T, X, U) -> () + SendSync + 'static, + ) where + T: Variant + Clone, + U: Variant + Clone, + X: Variant + Clone, + { + self.register_indexer_get(getter); + self.register_indexer_set(setter); } /// Compile a string into an `AST`, which can be used later for evaluation. diff --git a/src/engine.rs b/src/engine.rs index b79c497b..c10a89d4 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -74,9 +74,11 @@ pub const KEYWORD_EVAL: &str = "eval"; pub const FUNC_TO_STRING: &str = "to_string"; pub const FUNC_GETTER: &str = "get$"; pub const FUNC_SETTER: &str = "set$"; -pub const FUNC_INDEXER: &str = "$index$"; +pub const FUNC_INDEXER_GET: &str = "$index$get$"; +pub const FUNC_INDEXER_SET: &str = "$index$set$"; /// A type that encapsulates a mutation target for an expression with side effects. +#[derive(Debug)] enum Target<'a> { /// The target is a mutable reference to a `Dynamic` value somewhere. Ref(&'a mut Dynamic), @@ -91,40 +93,45 @@ impl Target<'_> { /// Is the `Target` a reference pointing to other data? pub fn is_ref(&self) -> bool { match self { - Target::Ref(_) => true, - Target::Value(_) | Target::StringChar(_, _, _) => false, + Self::Ref(_) => true, + Self::Value(_) | Self::StringChar(_, _, _) => false, + } + } + /// Is the `Target` an owned value? + pub fn is_value(&self) -> bool { + match self { + Self::Ref(_) => false, + Self::Value(_) => true, + Self::StringChar(_, _, _) => false, } } - /// Get the value of the `Target` as a `Dynamic`, cloning a referenced value if necessary. pub fn clone_into_dynamic(self) -> Dynamic { match self { - Target::Ref(r) => r.clone(), // Referenced value is cloned - Target::Value(v) => v, // Owned value is simply taken - Target::StringChar(_, _, ch) => ch, // Character is taken + Self::Ref(r) => r.clone(), // Referenced value is cloned + Self::Value(v) => v, // Owned value is simply taken + Self::StringChar(_, _, ch) => ch, // Character is taken } } - /// Get a mutable reference from the `Target`. pub fn as_mut(&mut self) -> &mut Dynamic { match self { - Target::Ref(r) => *r, - Target::Value(ref mut r) => r, - Target::StringChar(_, _, ref mut r) => r, + Self::Ref(r) => *r, + Self::Value(ref mut r) => r, + Self::StringChar(_, _, ref mut r) => r, } } - /// Update the value of the `Target`. /// Position in `EvalAltResult` is None and must be set afterwards. pub fn set_value(&mut self, new_val: Dynamic) -> Result<(), Box> { match self { - Target::Ref(r) => **r = new_val, - Target::Value(_) => { + Self::Ref(r) => **r = new_val, + Self::Value(_) => { return Err(Box::new(EvalAltResult::ErrorAssignmentToUnknownLHS( Position::none(), ))) } - Target::StringChar(Dynamic(Union::Str(ref mut s)), index, _) => { + Self::StringChar(Dynamic(Union::Str(ref mut s)), index, _) => { // Replace the character at the specified index position let new_ch = new_val .as_char() @@ -163,20 +170,17 @@ impl> From for Target<'_> { /// /// This type uses some unsafe code, mainly for avoiding cloning of local variable names via /// direct lifetime casting. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Eq, PartialEq, Hash, Clone, Default)] pub struct State { /// Normally, access to variables are parsed with a relative offset into the scope to avoid a lookup. /// In some situation, e.g. after running an `eval` statement, subsequent offsets become mis-aligned. /// When that happens, this flag is turned on to force a scope lookup by name. pub always_search: bool, - /// Level of the current scope. The global (root) level is zero, a new block (or function call) /// is one level higher, and so on. pub scope_level: usize, - /// Number of operations performed. pub operations: u64, - /// Number of modules loaded. pub modules: u64, } @@ -361,14 +365,23 @@ fn search_scope<'s, 'a>( _ => unreachable!(), }; + // Check if it is qualified if let Some(modules) = modules.as_ref() { - let module = if let Some(index) = modules.index() { + // Qualified - check if the root module is directly indexed + let index = if state.always_search { + None + } else { + modules.index() + }; + + let module = if let Some(index) = index { scope .get_mut(scope.len() - index.get()) .0 .downcast_mut::() .unwrap() } else { + // Find the root module in the scope let (id, root_pos) = modules.get(0); scope @@ -376,28 +389,27 @@ fn search_scope<'s, 'a>( .ok_or_else(|| Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)))? }; - return Ok(( - module.get_qualified_var_mut(name, *hash_var, *pos)?, - name, - // Module variables are constant - ScopeEntryType::Constant, - *pos, - )); - } + let target = module.get_qualified_var_mut(name, *hash_var, *pos)?; - let index = if state.always_search { None } else { *index }; - - let index = if let Some(index) = index { - scope.len() - index.get() + // Module variables are constant + Ok((target, name, ScopeEntryType::Constant, *pos)) } else { - scope - .get_index(name) - .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.into(), *pos)))? - .0 - }; + // Unqualified - check if it is directly indexed + let index = if state.always_search { None } else { *index }; - let (val, typ) = scope.get_mut(index); - Ok((val, name, typ, *pos)) + let index = if let Some(index) = index { + scope.len() - index.get() + } else { + // Find the variable in the scope + scope + .get_index(name) + .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.into(), *pos)))? + .0 + }; + + let (val, typ) = scope.get_mut(index); + Ok((val, name, typ, *pos)) + } } impl Engine { @@ -585,10 +597,16 @@ impl Engine { .or_else(|| self.packages.get_fn(hashes.0)); if let Some(func) = func { + // Calling pure function in method-call? + normalize_first_arg( + (func.is_pure() || func.is_script()) && is_ref, + &mut this_copy, + &mut old_this_ptr, + args, + ); + if func.is_script() { // Run scripted function - normalize_first_arg(is_ref, &mut this_copy, &mut old_this_ptr, args); - let fn_def = func.get_fn_def(); let result = self.call_script_fn(scope, state, lib, fn_name, fn_def, args, level)?; @@ -598,14 +616,6 @@ impl Engine { return Ok((result, false)); } else { - // Calling pure function in method-call? - normalize_first_arg( - func.is_pure() && is_ref, - &mut this_copy, - &mut old_this_ptr, - args, - ); - // Run external function let result = func.get_native_fn()(args)?; @@ -668,22 +678,40 @@ impl Engine { ))); } - let types_list: Vec<_> = args - .iter() - .map(|name| self.map_type_name(name.type_name())) - .collect(); - - // Getter function not found? - if fn_name == FUNC_INDEXER { + // index getter function not found? + if fn_name == FUNC_INDEXER_GET && args.len() == 2 { return Err(Box::new(EvalAltResult::ErrorFunctionNotFound( - format!("[]({})", types_list.join(", ")), + format!( + "{} [{}]", + self.map_type_name(args[0].type_name()), + self.map_type_name(args[1].type_name()), + ), + Position::none(), + ))); + } + + // index setter function not found? + if fn_name == FUNC_INDEXER_SET { + return Err(Box::new(EvalAltResult::ErrorFunctionNotFound( + format!( + "{} [{}]=", + self.map_type_name(args[0].type_name()), + self.map_type_name(args[1].type_name()), + ), Position::none(), ))); } // Raise error Err(Box::new(EvalAltResult::ErrorFunctionNotFound( - format!("{} ({})", fn_name, types_list.join(", ")), + format!( + "{} ({})", + fn_name, + args.iter() + .map(|name| self.map_type_name(name.type_name())) + .collect::>() + .join(", ") + ), Position::none(), ))) } @@ -787,12 +815,8 @@ impl Engine { level: usize, ) -> Result<(Dynamic, bool), Box> { // Qualifiers (none) + function name + number of arguments + argument `TypeId`'s. - let hash_fn = calc_fn_hash( - empty(), - fn_name, - args.len(), - args.iter().map(|a| a.type_id()), - ); + let arg_types = args.iter().map(|a| a.type_id()); + let hash_fn = calc_fn_hash(empty(), fn_name, args.len(), arg_types); let hashes = (hash_fn, if native_only { 0 } else { hash_fn_def }); match fn_name { @@ -890,8 +914,8 @@ impl Engine { let (idx, expr, pos) = x.as_ref(); let is_idx = matches!(rhs, Expr::Index(_)); let idx_pos = idx.position(); - let this_ptr = &mut self - .get_indexed_mut(state, lib, obj, is_ref, idx_val, idx_pos, false)?; + let this_ptr = + &mut self.get_indexed_mut(state, lib, target, idx_val, idx_pos, false)?; self.eval_dot_index_chain_helper( state, lib, this_ptr, expr, idx_values, is_idx, level, new_val, @@ -900,17 +924,52 @@ impl Engine { } // xxx[rhs] = new_val _ if new_val.is_some() => { - let this_ptr = - &mut self.get_indexed_mut(state, lib, obj, is_ref, idx_val, pos, true)?; + let mut idx_val2 = idx_val.clone(); - this_ptr - .set_value(new_val.unwrap()) - .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; - Ok((Default::default(), true)) + match self.get_indexed_mut(state, lib, target, idx_val, pos, true) { + // Indexed value is an owned value - the only possibility is an indexer + // Try to call an index setter + Ok(this_ptr) if this_ptr.is_value() => { + let fn_name = FUNC_INDEXER_SET; + let args = &mut [target.as_mut(), &mut idx_val2, &mut new_val.unwrap()]; + + self.exec_fn_call(state, lib, fn_name, true, 0, args, is_ref, None, 0) + .or_else(|err| match *err { + // If there is no index setter, no need to set it back because the indexer is read-only + EvalAltResult::ErrorFunctionNotFound(s, _) + if s == FUNC_INDEXER_SET => + { + Ok(Default::default()) + } + _ => Err(err), + })?; + } + // Indexed value is a reference - update directly + Ok(ref mut this_ptr) => { + this_ptr + .set_value(new_val.unwrap()) + .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; + } + Err(err) => match *err { + // No index getter - try to call an index setter + EvalAltResult::ErrorIndexingType(_, _) => { + let fn_name = FUNC_INDEXER_SET; + let args = + &mut [target.as_mut(), &mut idx_val2, &mut new_val.unwrap()]; + + self.exec_fn_call( + state, lib, fn_name, true, 0, args, is_ref, None, 0, + )?; + } + // Error + err => return Err(Box::new(err)), + }, + } + Ok(Default::default()) } // xxx[rhs] _ => self - .get_indexed_mut(state, lib, obj, is_ref, idx_val, pos, false) + .get_indexed_mut(state, lib, target, idx_val, pos, false) .map(|v| (v.clone_into_dynamic(), false)), } } else { @@ -940,8 +999,7 @@ impl Engine { Expr::Property(x) if obj.is::() && new_val.is_some() => { let ((prop, _, _), pos) = x.as_ref(); let index = prop.clone().into(); - let mut val = - self.get_indexed_mut(state, lib, obj, is_ref, index, *pos, true)?; + let mut val = self.get_indexed_mut(state, lib, target, index, *pos, true)?; val.set_value(new_val.unwrap()) .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; @@ -952,7 +1010,7 @@ impl Engine { Expr::Property(x) if obj.is::() => { let ((prop, _, _), pos) = x.as_ref(); let index = prop.clone().into(); - let val = self.get_indexed_mut(state, lib, obj, is_ref, index, *pos, false)?; + let val = self.get_indexed_mut(state, lib, target, index, *pos, false)?; Ok((val.clone_into_dynamic(), false)) } @@ -981,7 +1039,7 @@ impl Engine { let mut val = if let Expr::Property(p) = prop { let ((prop, _, _), _) = p.as_ref(); let index = prop.clone().into(); - self.get_indexed_mut(state, lib, obj, is_ref, index, *pos, false)? + self.get_indexed_mut(state, lib, target, index, *pos, false)? } else { unreachable!(); }; @@ -1160,14 +1218,16 @@ impl Engine { &self, state: &mut State, lib: &Module, - val: &'a mut Dynamic, - is_ref: bool, + target: &'a mut Target, mut idx: Dynamic, idx_pos: Position, create: bool, ) -> Result, Box> { self.inc_operations(state)?; + let is_ref = target.is_ref(); + let val = target.as_mut(); + match val { #[cfg(not(feature = "no_index"))] Dynamic(Union::Array(arr)) => { @@ -1233,7 +1293,7 @@ impl Engine { } _ => { - let fn_name = FUNC_INDEXER; + let fn_name = FUNC_INDEXER_GET; let type_name = self.map_type_name(val.type_name()); let args = &mut [val, &mut idx]; self.exec_fn_call(state, lib, fn_name, true, 0, args, is_ref, None, 0) diff --git a/src/module.rs b/src/module.rs index d509f3c1..2a722e3b 100644 --- a/src/module.rs +++ b/src/module.rs @@ -2,7 +2,7 @@ use crate::any::{Dynamic, Variant}; use crate::calc_fn_hash; -use crate::engine::{make_getter, make_setter, Engine, FUNC_INDEXER}; +use crate::engine::{make_getter, make_setter, Engine, FUNC_INDEXER_GET, FUNC_INDEXER_SET}; use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync}; use crate::parser::{ FnAccess, @@ -518,7 +518,7 @@ impl Module { self.set_fn_2_mut(make_setter(&name.into()), func) } - /// Set a Rust indexer function taking two parameters (the first one mutable) into the module, + /// Set a Rust index getter taking two parameters (the first one mutable) into the module, /// returning a hash key. /// /// If there is a similar existing setter Rust function, it is replaced. @@ -529,19 +529,19 @@ impl Module { /// use rhai::{Module, ImmutableString}; /// /// let mut module = Module::new(); - /// let hash = module.set_indexer_fn(|x: &mut i64, y: ImmutableString| { + /// let hash = module.set_indexer_get_fn(|x: &mut i64, y: ImmutableString| { /// Ok(*x + y.len() as i64) /// }); /// assert!(module.get_fn(hash).is_some()); /// ``` #[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_index"))] - pub fn set_indexer_fn( + pub fn set_indexer_get_fn( &mut self, #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B) -> FuncReturn + 'static, #[cfg(feature = "sync")] func: impl Fn(&mut A, B) -> FuncReturn + Send + Sync + 'static, ) -> u64 { - self.set_fn_2_mut(FUNC_INDEXER, func) + self.set_fn_2_mut(FUNC_INDEXER_GET, func) } /// Set a Rust function taking three parameters into the module, returning a hash key. @@ -629,6 +629,44 @@ impl Module { ) } + /// Set a Rust index setter taking three parameters (the first one mutable) into the module, + /// returning a hash key. + /// + /// If there is a similar existing Rust function, it is replaced. + /// + /// # Examples + /// + /// ``` + /// use rhai::{Module, ImmutableString}; + /// + /// let mut module = Module::new(); + /// let hash = module.set_indexer_set_fn(|x: &mut i64, y: ImmutableString, value: i64| { + /// *x = y.len() as i64 + value; + /// Ok(()) + /// }); + /// assert!(module.get_fn(hash).is_some()); + /// ``` + pub fn set_indexer_set_fn( + &mut self, + #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B, A) -> FuncReturn<()> + 'static, + #[cfg(feature = "sync")] func: impl Fn(&mut A, B, A) -> FuncReturn<()> + Send + Sync + 'static, + ) -> u64 { + let f = move |args: &mut FnCallArgs| { + let b = mem::take(args[1]).cast::(); + let c = mem::take(args[2]).cast::
(); + let a = args[0].downcast_mut::().unwrap(); + + func(a, b, c).map(Dynamic::from) + }; + let args = [TypeId::of::(), TypeId::of::(), TypeId::of::()]; + self.set_fn( + FUNC_INDEXER_SET, + Public, + &args, + CallableFunction::from_method(Box::new(f)), + ) + } + /// Set a Rust function taking four parameters into the module, returning a hash key. /// /// If there is a similar existing Rust function, it is replaced. diff --git a/tests/get_set.rs b/tests/get_set.rs index 2df59671..ca5c0c47 100644 --- a/tests/get_set.rs +++ b/tests/get_set.rs @@ -42,18 +42,25 @@ fn test_get_set() -> Result<(), Box> { engine.register_fn("add", |value: &mut INT| *value += 41); engine.register_fn("new_ts", TestStruct::new); - #[cfg(not(feature = "no_index"))] - engine.register_indexer(|value: &mut TestStruct, index: ImmutableString| { - value.array[index.len()] - }); - assert_eq!(engine.eval::("let a = new_ts(); a.x = 500; a.x")?, 500); assert_eq!(engine.eval::("let a = new_ts(); a.x.add(); a.x")?, 42); assert_eq!(engine.eval::("let a = new_ts(); a.y.add(); a.y")?, 0); #[cfg(not(feature = "no_index"))] - assert_eq!(engine.eval::(r#"let a = new_ts(); a["abc"]"#)?, 4); + { + engine.register_indexer_get_set( + |value: &mut TestStruct, index: ImmutableString| value.array[index.len()], + |value: &mut TestStruct, index: ImmutableString, new_val: INT| { + value.array[index.len()] = new_val + }, + ); + assert_eq!(engine.eval::(r#"let a = new_ts(); a["abc"]"#)?, 4); + assert_eq!( + engine.eval::(r#"let a = new_ts(); a["abc"] = 42; a["abc"]"#)?, + 42 + ); + } Ok(()) } From e942ef358c7be0ed5372e4dd3607ddb8b6ca658f Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 7 Jun 2020 17:54:33 +0800 Subject: [PATCH 17/53] Transparently convert &str to ImmutableString for register_fn. --- README.md | 44 ++++++++++++++++++++++++++++++++++++++------ RELEASES.md | 1 + src/engine.rs | 10 ++++++++++ src/fn_register.rs | 30 +++++++++++++++++++++++++----- tests/string.rs | 21 ++++++++++++++++++++- 5 files changed, 94 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 39539e54..4fb86ec8 100644 --- a/README.md +++ b/README.md @@ -684,13 +684,18 @@ Rhai's scripting engine is very lightweight. It gets most of its abilities from To call these functions, they need to be registered with the [`Engine`]. ```rust -use rhai::{Dynamic, Engine, EvalAltResult}; +use rhai::{Dynamic, Engine, EvalAltResult, ImmutableString}; use rhai::RegisterFn; // use 'RegisterFn' trait for 'register_fn' use rhai::RegisterResultFn; // use 'RegisterResultFn' trait for 'register_result_fn' -// Normal function that returns any value type -fn add(x: i64, y: i64) -> i64 { - x + y +// Normal function that returns a standard type +// Remember to use 'ImmutableString' and not 'String' +fn add_len(x: i64, s: ImmutableString) -> i64 { + x + s.len() +} +// Alternatively, '&str' maps directly to 'ImmutableString' +fn add_len_str(x: i64, s: &str) -> i64 { + x + s.len() } // Function that returns a 'Dynamic' value - must return a 'Result' @@ -702,9 +707,14 @@ fn main() -> Result<(), Box> { let engine = Engine::new(); - engine.register_fn("add", add); + engine.register_fn("add", add_len); + engine.register_fn("add_str", add_len_str); - let result = engine.eval::("add(40, 2)")?; + let result = engine.eval::(r#"add(40, "xx")"#)?; + + println!("Answer: {}", result); // prints 42 + + let result = engine.eval::(r#"add_str(40, "xx")"#)?; println!("Answer: {}", result); // prints 42 @@ -735,6 +745,25 @@ i.e. different functions can have the same name as long as their parameters are and/or different number. New definitions _overwrite_ previous definitions of the same name and same number/types of parameters. +### `String` parameters + +Functions accepting a parameter of `String` should use `&str` instead because it maps directly to `ImmutableString` +which is the type that Rhai uses to represent strings internally. + +```rust +fn get_len1(s: String) -> i64 { s.len() as i64 } // <- Rhai will not find this function +fn get_len2(s: &str) -> i64 { s.len() as i64 } // <- Rhai finds this function fine +fn get_len3(s: ImmutableString) -> i64 { s.len() as i64 } // <- the above is equivalent to this + +engine.register_fn("len1", get_len1); +engine.register_fn("len2", get_len2); +engine.register_fn("len3", get_len3); + +let len = engine.eval::("x.len1()")?; // error: function 'len1 (string)' not found +let len = engine.eval::("x.len2()")?; // works fine +let len = engine.eval::("x.len3()")?; // works fine +``` + Generic functions ----------------- @@ -1388,6 +1417,9 @@ Strings and Chars [strings]: #strings-and-chars [char]: #strings-and-chars +All strings in Rhai are implemented as `ImmutableString` (see [standard types]). +`ImmutableString` should be used in place of the standard Rust type `String` when registering functions. + String and character literals follow C-style formatting, with support for Unicode ('`\u`_xxxx_' or '`\U`_xxxxxxxx_') and hex ('`\x`_xx_') escape sequences. diff --git a/RELEASES.md b/RELEASES.md index 58303958..3622167a 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -15,6 +15,7 @@ New features ------------ * Indexers are now split into getters ans setters (which now support updates). The API is split into `Engine::register_indexer_get` and `Engine::register_indexer_set` with `Engine::register_indexer_get_set` being a shorthand. Similarly, `Module::set_indexer_get_fn` and `Module::set_indexer_set_fn` are added. +* `Engine:register_fn` and `Engine:register_result_fn` accepts functions that take parameters of type `&str` (immutable string slice), which maps directly to `ImmutableString`. This is to avoid needing wrappers for functions taking string parameters. Version 0.15.0 diff --git a/src/engine.rs b/src/engine.rs index c10a89d4..9be3c04c 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -906,6 +906,9 @@ impl Engine { let mut idx_val = idx_values.pop(); if is_index { + #[cfg(feature = "no_index")] + unreachable!(); + let pos = rhs.position(); match rhs { @@ -1292,6 +1295,7 @@ impl Engine { } } + #[cfg(not(feature = "no_index"))] _ => { let fn_name = FUNC_INDEXER_GET; let type_name = self.map_type_name(val.type_name()); @@ -1305,6 +1309,12 @@ impl Engine { )) }) } + + #[cfg(feature = "no_index")] + _ => Err(Box::new(EvalAltResult::ErrorIndexingType( + self.map_type_name(val.type_name()).into(), + Position::none(), + ))), } } diff --git a/src/fn_register.rs b/src/fn_register.rs index cb208d12..1384f666 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -7,6 +7,7 @@ use crate::engine::Engine; use crate::fn_native::{CallableFunction, FnAny, FnCallArgs}; use crate::parser::FnAccess; use crate::result::EvalAltResult; +use crate::utils::ImmutableString; use crate::stdlib::{any::TypeId, boxed::Box, mem}; @@ -99,9 +100,16 @@ pub fn by_ref(data: &mut Dynamic) -> &mut T { /// Dereference into value. #[inline(always)] pub fn by_value(data: &mut Dynamic) -> T { - // We consume the argument and then replace it with () - the argument is not supposed to be used again. - // This way, we avoid having to clone the argument again, because it is already a clone when passed here. - mem::take(data).cast::() + if TypeId::of::() == TypeId::of::<&str>() { + // &str parameters are mapped to the underlying ImmutableString + let r = data.as_str().unwrap(); + let x = unsafe { mem::transmute::<_, &T>(&r) }; + x.clone() + } else { + // We consume the argument and then replace it with () - the argument is not supposed to be used again. + // This way, we avoid having to clone the argument again, because it is already a clone when passed here. + mem::take(data).cast::() + } } /// This macro creates a closure wrapping a registered function. @@ -146,6 +154,18 @@ pub fn map_result( data } +/// Remap `&str` to `ImmutableString`. +#[inline(always)] +fn map_type_id() -> TypeId { + let id = TypeId::of::(); + + if id == TypeId::of::<&str>() { + TypeId::of::() + } else { + id + } +} + macro_rules! def_register { () => { def_register!(imp from_pure :); @@ -170,7 +190,7 @@ macro_rules! def_register { { fn register_fn(&mut self, name: &str, f: FN) { self.global_module.set_fn(name, FnAccess::Public, - &[$(TypeId::of::<$par>()),*], + &[$(map_type_id::<$par>()),*], CallableFunction::$abi(make_func!(f : map_dynamic ; $($par => $clone),*)) ); } @@ -187,7 +207,7 @@ macro_rules! def_register { { fn register_result_fn(&mut self, name: &str, f: FN) { self.global_module.set_fn(name, FnAccess::Public, - &[$(TypeId::of::<$par>()),*], + &[$(map_type_id::<$par>()),*], CallableFunction::$abi(make_func!(f : map_result ; $($par => $clone),*)) ); } diff --git a/tests/string.rs b/tests/string.rs index 995bd76e..27854629 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -1,4 +1,4 @@ -use rhai::{Engine, EvalAltResult, INT}; +use rhai::{Engine, EvalAltResult, ImmutableString, RegisterFn, INT}; #[test] fn test_string() -> Result<(), Box> { @@ -154,3 +154,22 @@ fn test_string_substring() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_string_fn() -> Result<(), Box> { + let mut engine = Engine::new(); + + engine.register_fn("foo1", |s: &str| s.len() as INT); + engine.register_fn("foo2", |s: ImmutableString| s.len() as INT); + engine.register_fn("foo3", |s: String| s.len() as INT); + + assert_eq!(engine.eval::(r#"foo1("hello")"#)?, 5); + assert_eq!(engine.eval::(r#"foo2("hello")"#)?, 5); + + assert!(matches!( + *engine.eval::(r#"foo3("hello")"#).expect_err("should error"), + EvalAltResult::ErrorFunctionNotFound(ref x, _) if x == "foo3 (string)" + )); + + Ok(()) +} From 5fb4b04cb015d8ae973ce740b128cea85bc63ee7 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 8 Jun 2020 10:26:12 +0800 Subject: [PATCH 18/53] Put type on transmute call. --- src/utils.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index ccd3d6be..4a8cd3c8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -190,8 +190,9 @@ impl Clone for StaticVec { if self.is_fixed_storage() { for x in 0..self.len { - let item: &T = unsafe { mem::transmute(self.list.get(x).unwrap()) }; - value.list[x] = MaybeUninit::new(item.clone()); + let item = self.list.get(x).unwrap(); + let item_value = unsafe { mem::transmute::<_, &T>(item) }; + value.list[x] = MaybeUninit::new(item_value.clone()); } } else { value.more = self.more.clone(); @@ -424,7 +425,7 @@ impl StaticVec { panic!("index OOB in StaticVec"); } - let list: &[T; MAX_STATIC_VEC] = unsafe { mem::transmute(&self.list) }; + let list = unsafe { mem::transmute::<_, &[T; MAX_STATIC_VEC]>(&self.list) }; if self.is_fixed_storage() { list.get(index).unwrap() @@ -442,7 +443,7 @@ impl StaticVec { panic!("index OOB in StaticVec"); } - let list: &mut [T; MAX_STATIC_VEC] = unsafe { mem::transmute(&mut self.list) }; + let list = unsafe { mem::transmute::<_, &mut [T; MAX_STATIC_VEC]>(&mut self.list) }; if self.is_fixed_storage() { list.get_mut(index).unwrap() @@ -452,7 +453,7 @@ impl StaticVec { } /// Get an iterator to entries in the `StaticVec`. pub fn iter(&self) -> impl Iterator { - let list: &[T; MAX_STATIC_VEC] = unsafe { mem::transmute(&self.list) }; + let list = unsafe { mem::transmute::<_, &[T; MAX_STATIC_VEC]>(&self.list) }; if self.is_fixed_storage() { list[..self.len].iter() @@ -462,7 +463,7 @@ impl StaticVec { } /// Get a mutable iterator to entries in the `StaticVec`. pub fn iter_mut(&mut self) -> impl Iterator { - let list: &mut [T; MAX_STATIC_VEC] = unsafe { mem::transmute(&mut self.list) }; + let list = unsafe { mem::transmute::<_, &mut [T; MAX_STATIC_VEC]>(&mut self.list) }; if self.is_fixed_storage() { list[..self.len].iter_mut() @@ -549,7 +550,7 @@ impl fmt::Debug for StaticVec { impl AsRef<[T]> for StaticVec { fn as_ref(&self) -> &[T] { - let list: &[T; MAX_STATIC_VEC] = unsafe { mem::transmute(&self.list) }; + let list = unsafe { mem::transmute::<_, &[T; MAX_STATIC_VEC]>(&self.list) }; if self.is_fixed_storage() { &list[..self.len] @@ -561,7 +562,7 @@ impl AsRef<[T]> for StaticVec { impl AsMut<[T]> for StaticVec { fn as_mut(&mut self) -> &mut [T] { - let list: &mut [T; MAX_STATIC_VEC] = unsafe { mem::transmute(&mut self.list) }; + let list = unsafe { mem::transmute::<_, &mut [T; MAX_STATIC_VEC]>(&mut self.list) }; if self.is_fixed_storage() { &mut list[..self.len] From ead366aac80b51298aea655d699a84f7fc8aeff9 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 8 Jun 2020 10:26:32 +0800 Subject: [PATCH 19/53] Better String parameter error message. --- src/engine.rs | 6 +++++- src/fn_register.rs | 8 ++++---- tests/string.rs | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 9be3c04c..4ad31b36 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -708,7 +708,11 @@ impl Engine { "{} ({})", fn_name, args.iter() - .map(|name| self.map_type_name(name.type_name())) + .map(|name| if name.is::() { + "&str | ImmutableString" + } else { + self.map_type_name(name.type_name()) + }) .collect::>() .join(", ") ), diff --git a/src/fn_register.rs b/src/fn_register.rs index 1384f666..1285c119 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -101,10 +101,10 @@ pub fn by_ref(data: &mut Dynamic) -> &mut T { #[inline(always)] pub fn by_value(data: &mut Dynamic) -> T { if TypeId::of::() == TypeId::of::<&str>() { - // &str parameters are mapped to the underlying ImmutableString - let r = data.as_str().unwrap(); - let x = unsafe { mem::transmute::<_, &T>(&r) }; - x.clone() + // If T is &str, data must be ImmutableString, so map directly to it + let ref_str = data.as_str().unwrap(); + let ref_T = unsafe { mem::transmute::<_, &T>(&ref_str) }; + ref_T.clone() } else { // We consume the argument and then replace it with () - the argument is not supposed to be used again. // This way, we avoid having to clone the argument again, because it is already a clone when passed here. diff --git a/tests/string.rs b/tests/string.rs index 27854629..c93278de 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -168,7 +168,7 @@ fn test_string_fn() -> Result<(), Box> { assert!(matches!( *engine.eval::(r#"foo3("hello")"#).expect_err("should error"), - EvalAltResult::ErrorFunctionNotFound(ref x, _) if x == "foo3 (string)" + EvalAltResult::ErrorFunctionNotFound(ref x, _) if x == "foo3 (&str | ImmutableString)" )); Ok(()) From a4cabc1ac71f9a3ff2b1c44a68535738c231ffeb Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 8 Jun 2020 14:10:06 +0800 Subject: [PATCH 20/53] Better String parameter error message. --- src/engine.rs | 6 +++++- tests/decrement.rs | 2 +- tests/string.rs | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 9be3c04c..4ad31b36 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -708,7 +708,11 @@ impl Engine { "{} ({})", fn_name, args.iter() - .map(|name| self.map_type_name(name.type_name())) + .map(|name| if name.is::() { + "&str | ImmutableString" + } else { + self.map_type_name(name.type_name()) + }) .collect::>() .join(", ") ), diff --git a/tests/decrement.rs b/tests/decrement.rs index ab1f9376..1b74dfe9 100644 --- a/tests/decrement.rs +++ b/tests/decrement.rs @@ -8,7 +8,7 @@ fn test_decrement() -> Result<(), Box> { assert!(matches!( *engine.eval::(r#"let s = "test"; s -= "ing"; s"#).expect_err("expects error"), - EvalAltResult::ErrorFunctionNotFound(err, _) if err == "- (string, string)" + EvalAltResult::ErrorFunctionNotFound(err, _) if err == "- (&str | ImmutableString, &str | ImmutableString)" )); Ok(()) diff --git a/tests/string.rs b/tests/string.rs index 27854629..6eed9e6a 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -168,7 +168,7 @@ fn test_string_fn() -> Result<(), Box> { assert!(matches!( *engine.eval::(r#"foo3("hello")"#).expect_err("should error"), - EvalAltResult::ErrorFunctionNotFound(ref x, _) if x == "foo3 (string)" + EvalAltResult::ErrorFunctionNotFound(err, _) if err == "foo3 (&str | ImmutableString)" )); Ok(()) From b4b835f80abeba768483e6f2e1685084da16c0d7 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 8 Jun 2020 14:10:16 +0800 Subject: [PATCH 21/53] Remove unnecessary "sync" feature gates. --- src/any.rs | 29 ++--------------------------- src/fn_register.rs | 24 +++++++----------------- src/module.rs | 39 +++++++++++++-------------------------- 3 files changed, 22 insertions(+), 70 deletions(-) diff --git a/src/any.rs b/src/any.rs index 0b6d81d2..0647d219 100644 --- a/src/any.rs +++ b/src/any.rs @@ -1,5 +1,6 @@ //! Helper module which defines the `Any` trait to to allow dynamic value handling. +use crate::fn_native::SendSync; use crate::module::Module; use crate::parser::{ImmutableString, INT}; use crate::r#unsafe::{unsafe_cast_box, unsafe_try_cast}; @@ -54,31 +55,6 @@ pub trait Variant: Any { fn _closed(&self) -> _Private; } -#[cfg(not(feature = "sync"))] -impl Variant for T { - fn as_any(&self) -> &dyn Any { - self as &dyn Any - } - fn as_mut_any(&mut self) -> &mut dyn Any { - self as &mut dyn Any - } - fn as_box_any(self: Box) -> Box { - self as Box - } - fn type_name(&self) -> &'static str { - type_name::() - } - fn into_dynamic(self) -> Dynamic { - Dynamic::from(self) - } - fn clone_into_dynamic(&self) -> Dynamic { - Dynamic::from(self.clone()) - } - fn _closed(&self) -> _Private { - _Private - } -} - /// Trait to represent any type. /// /// `From<_>` is implemented for `i64` (`i32` if `only_i32`), `f64` (if not `no_float`), @@ -108,8 +84,7 @@ pub trait Variant: Any + Send + Sync { fn _closed(&self) -> _Private; } -#[cfg(feature = "sync")] -impl Variant for T { +impl Variant for T { fn as_any(&self) -> &dyn Any { self as &dyn Any } diff --git a/src/fn_register.rs b/src/fn_register.rs index 1384f666..86e7ada9 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -4,7 +4,7 @@ use crate::any::{Dynamic, Variant}; use crate::engine::Engine; -use crate::fn_native::{CallableFunction, FnAny, FnCallArgs}; +use crate::fn_native::{CallableFunction, FnAny, FnCallArgs, SendSync}; use crate::parser::FnAccess; use crate::result::EvalAltResult; use crate::utils::ImmutableString; @@ -101,10 +101,10 @@ pub fn by_ref(data: &mut Dynamic) -> &mut T { #[inline(always)] pub fn by_value(data: &mut Dynamic) -> T { if TypeId::of::() == TypeId::of::<&str>() { - // &str parameters are mapped to the underlying ImmutableString - let r = data.as_str().unwrap(); - let x = unsafe { mem::transmute::<_, &T>(&r) }; - x.clone() + // If T is &str, data must be ImmutableString, so map directly to it + let ref_str = data.as_str().unwrap(); + let ref_T = unsafe { mem::transmute::<_, &T>(&ref_str) }; + ref_T.clone() } else { // We consume the argument and then replace it with () - the argument is not supposed to be used again. // This way, we avoid having to clone the argument again, because it is already a clone when passed here. @@ -178,13 +178,7 @@ macro_rules! def_register { // ^ dereferencing function impl< $($par: Variant + Clone,)* - - #[cfg(feature = "sync")] - FN: Fn($($param),*) -> RET + Send + Sync + 'static, - - #[cfg(not(feature = "sync"))] - FN: Fn($($param),*) -> RET + 'static, - + FN: Fn($($param),*) -> RET + SendSync + 'static, RET: Variant + Clone > RegisterFn for Engine { @@ -198,11 +192,7 @@ macro_rules! def_register { impl< $($par: Variant + Clone,)* - - #[cfg(feature = "sync")] - FN: Fn($($param),*) -> Result> + Send + Sync + 'static, - #[cfg(not(feature = "sync"))] - FN: Fn($($param),*) -> Result> + 'static, + FN: Fn($($param),*) -> Result> + SendSync + 'static, > RegisterResultFn for Engine { fn register_result_fn(&mut self, name: &str, f: FN) { diff --git a/src/module.rs b/src/module.rs index 2a722e3b..1deddee4 100644 --- a/src/module.rs +++ b/src/module.rs @@ -321,8 +321,7 @@ impl Module { pub fn set_fn_0( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn() -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn() -> FuncReturn + Send + Sync + 'static, + func: impl Fn() -> FuncReturn + SendSync + 'static, ) -> u64 { let f = move |_: &mut FnCallArgs| func().map(Dynamic::from); let args = []; @@ -350,8 +349,7 @@ impl Module { pub fn set_fn_1( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(A) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(A) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(A) -> FuncReturn + SendSync + 'static, ) -> u64 { let f = move |args: &mut FnCallArgs| func(mem::take(args[0]).cast::()).map(Dynamic::from); @@ -380,8 +378,7 @@ impl Module { pub fn set_fn_1_mut( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(&mut A) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(&mut A) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(&mut A) -> FuncReturn + SendSync + 'static, ) -> u64 { let f = move |args: &mut FnCallArgs| { func(args[0].downcast_mut::().unwrap()).map(Dynamic::from) @@ -412,8 +409,7 @@ impl Module { pub fn set_getter_fn( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(&mut A) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(&mut A) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(&mut A) -> FuncReturn + SendSync + 'static, ) -> u64 { self.set_fn_1_mut(make_getter(&name.into()), func) } @@ -436,8 +432,7 @@ impl Module { pub fn set_fn_2( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(A, B) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(A, B) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(A, B) -> FuncReturn + SendSync + 'static, ) -> u64 { let f = move |args: &mut FnCallArgs| { let a = mem::take(args[0]).cast::(); @@ -473,8 +468,7 @@ impl Module { pub fn set_fn_2_mut( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(&mut A, B) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(&mut A, B) -> FuncReturn + SendSync + 'static, ) -> u64 { let f = move |args: &mut FnCallArgs| { let b = mem::take(args[1]).cast::(); @@ -512,8 +506,7 @@ impl Module { pub fn set_setter_fn( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B) -> FuncReturn<()> + 'static, - #[cfg(feature = "sync")] func: impl Fn(&mut A, B) -> FuncReturn<()> + Send + Sync + 'static, + func: impl Fn(&mut A, B) -> FuncReturn<()> + SendSync + 'static, ) -> u64 { self.set_fn_2_mut(make_setter(&name.into()), func) } @@ -538,8 +531,7 @@ impl Module { #[cfg(not(feature = "no_index"))] pub fn set_indexer_get_fn( &mut self, - #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(&mut A, B) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(&mut A, B) -> FuncReturn + SendSync + 'static, ) -> u64 { self.set_fn_2_mut(FUNC_INDEXER_GET, func) } @@ -567,8 +559,7 @@ impl Module { >( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(A, B, C) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(A, B, C) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(A, B, C) -> FuncReturn + SendSync + 'static, ) -> u64 { let f = move |args: &mut FnCallArgs| { let a = mem::take(args[0]).cast::(); @@ -610,8 +601,7 @@ impl Module { >( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B, C) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(&mut A, B, C) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(&mut A, B, C) -> FuncReturn + SendSync + 'static, ) -> u64 { let f = move |args: &mut FnCallArgs| { let b = mem::take(args[1]).cast::(); @@ -648,8 +638,7 @@ impl Module { /// ``` pub fn set_indexer_set_fn( &mut self, - #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B, A) -> FuncReturn<()> + 'static, - #[cfg(feature = "sync")] func: impl Fn(&mut A, B, A) -> FuncReturn<()> + Send + Sync + 'static, + func: impl Fn(&mut A, B, A) -> FuncReturn<()> + SendSync + 'static, ) -> u64 { let f = move |args: &mut FnCallArgs| { let b = mem::take(args[1]).cast::(); @@ -691,8 +680,7 @@ impl Module { >( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(A, B, C, D) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(A, B, C, D) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(A, B, C, D) -> FuncReturn + SendSync + 'static, ) -> u64 { let f = move |args: &mut FnCallArgs| { let a = mem::take(args[0]).cast::(); @@ -741,8 +729,7 @@ impl Module { >( &mut self, name: impl Into, - #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B, C, D) -> FuncReturn + 'static, - #[cfg(feature = "sync")] func: impl Fn(&mut A, B, C, D) -> FuncReturn + Send + Sync + 'static, + func: impl Fn(&mut A, B, C, D) -> FuncReturn + SendSync + 'static, ) -> u64 { let f = move |args: &mut FnCallArgs| { let b = mem::take(args[1]).cast::(); From 95f94a33485cc5e6810cf90a22d37fc7b1450b2f Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 9 Jun 2020 12:21:21 +0800 Subject: [PATCH 22/53] Add strings example. --- README.md | 7 ++-- examples/strings.rs | 79 +++++++++++++++++++++++++++++++++++++++++++++ src/engine.rs | 2 +- 3 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 examples/strings.rs diff --git a/README.md b/README.md index 4fb86ec8..eee7c1c9 100644 --- a/README.md +++ b/README.md @@ -179,13 +179,14 @@ A number of examples can be found in the `examples` folder: | Example | Description | | ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| [`arrays_and_structs`](examples/arrays_and_structs.rs) | demonstrates registering a new type to Rhai and the usage of [arrays] on it | -| [`custom_types_and_methods`](examples/custom_types_and_methods.rs) | shows how to register a type and methods for it | +| [`arrays_and_structs`](examples/arrays_and_structs.rs) | shows how to register a custom Rust type and using [arrays] on it | +| [`custom_types_and_methods`](examples/custom_types_and_methods.rs) | shows how to register a custom Rust type and methods for it | | [`hello`](examples/hello.rs) | simple example that evaluates an expression and prints the result | | [`no_std`](examples/no_std.rs) | example to test out `no-std` builds | | [`reuse_scope`](examples/reuse_scope.rs) | evaluates two pieces of code in separate runs, but using a common [`Scope`] | | [`rhai_runner`](examples/rhai_runner.rs) | runs each filename passed to it as a Rhai script | -| [`simple_fn`](examples/simple_fn.rs) | shows how to register a Rust function to a Rhai [`Engine`] | +| [`simple_fn`](examples/simple_fn.rs) | shows how to register a simple function | +| [`strings`](examples/strings.rs) | shows different ways to register functions taking string arguments | | [`repl`](examples/repl.rs) | a simple REPL, interactively evaluate statements from stdin | Examples can be run with the following command: diff --git a/examples/strings.rs b/examples/strings.rs new file mode 100644 index 00000000..9e874377 --- /dev/null +++ b/examples/strings.rs @@ -0,0 +1,79 @@ +///! This example registers a variety of functions that operate on strings. +///! Remember to use `ImmutableString` or `&str` instead of `String` as parameters. +use rhai::{Engine, EvalAltResult, ImmutableString, RegisterFn, Scope, INT}; +use std::io::{stdin, stdout, Write}; + +/// Trim whitespace from a string. The original string argument is changed. +/// +/// This version uses `&mut ImmutableString` +fn trim_string(s: &mut ImmutableString) { + *s = s.trim().into(); +} + +/// Notice this is different from the built-in Rhai 'len' function for strings +/// which counts the actual number of Unicode _characters_ in a string. +/// This version simply counts the number of _bytes_ in the UTF-8 representation. +/// +/// This version uses `&str`. +fn count_string_bytes(s: &str) -> INT { + s.len() as INT +} + +/// This version uses `ImmutableString` and `&str`. +fn find_substring(s: ImmutableString, sub: &str) -> INT { + s.as_str().find(sub).map(|x| x as INT).unwrap_or(-1) +} + +fn main() -> Result<(), Box> { + // Create a `raw` Engine with no built-in string functions. + let mut engine = Engine::new_raw(); + + // Register string functions + engine.register_fn("trim", trim_string); + engine.register_fn("len", count_string_bytes); + engine.register_fn("index_of", find_substring); + + // Register string functions using closures + engine.register_fn("display", |label: &str, x: INT| { + println!("{}: {}", label, x) + }); + engine.register_fn("display", |label: ImmutableString, x: &str| { + println!(r#"{}: "{}""#, label, x) // Quote the input string + }); + + let mut scope = Scope::new(); + let mut input = String::new(); + + loop { + scope.clear(); + + println!("Type something. Press Ctrl-C to exit."); + print!("strings> "); + stdout().flush().expect("couldn't flush stdout"); + + input.clear(); + + if let Err(err) = stdin().read_line(&mut input) { + panic!("input error: {}", err); + } + + scope.push("x", input.clone()); + + println!("Line: {}", input.replace('\r', "\\r").replace('\n', "\\n")); + + engine.consume_with_scope( + &mut scope, + r#" + display("Length", x.len()); + x.trim(); + display("Trimmed", x); + display("Trimmed Length", x.len()); + display("Index of \"!!!\"", x.index_of("!!!")); + "#, + )?; + + println!(); + } + + Ok(()) +} diff --git a/src/engine.rs b/src/engine.rs index 4ad31b36..0408d50f 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -228,7 +228,7 @@ pub fn get_script_function_by_signature<'a>( /// # } /// ``` /// -/// Currently, `Engine` is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`. +/// Currently, `Engine` is neither `Send` nor `Sync`. Use the `sync` feature to make it `Send + Sync`. pub struct Engine { /// A module containing all functions directly loaded into the Engine. pub(crate) global_module: Module, From 13cde456e5cff1a7b4cc8046a19d21dc06e32216 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 10 Jun 2020 22:28:50 +0800 Subject: [PATCH 23/53] Change version to 0.15.1. --- Cargo.toml | 2 +- README.md | 18 +++++++++--------- RELEASES.md | 9 +++++++-- examples/strings.rs | 2 -- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 210b7488..5bae3961 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rhai" -version = "0.16.0" +version = "0.15.1" edition = "2018" authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung"] description = "Embedded scripting for Rust" diff --git a/README.md b/README.md index eee7c1c9..99ce9018 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Features to do checked arithmetic operations); for [`no-std`](#optional-features) builds, a number of additional dependencies are pulled in to provide for functionalities that used to be in `std`. -**Note:** Currently, the version is `0.16.0`, so the language and API's may change before they stabilize. +**Note:** Currently, the version is `0.15.1`, so the language and API's may change before they stabilize. What Rhai doesn't do -------------------- @@ -71,7 +71,7 @@ Install the Rhai crate on [`crates.io`](https::/crates.io/crates/rhai/) by addin ```toml [dependencies] -rhai = "0.16.0" +rhai = "0.15.1" ``` Use the latest released crate version on [`crates.io`](https::/crates.io/crates/rhai/): @@ -315,7 +315,7 @@ Functions declared with `private` are hidden and cannot be called from Rust (see ```rust // Define functions in a script. let ast = engine.compile(true, - r" + r#" // a function with two parameters: String and i64 fn hello(x, y) { x.len + y @@ -335,7 +335,7 @@ let ast = engine.compile(true, private hidden() { throw "you shouldn't see me!"; } - ")?; + "#)?; // A custom scope can also contain any variables/constants available to the functions let mut scope = Scope::new(); @@ -522,7 +522,7 @@ The following primitive types are supported natively: | **Floating-point number** (disabled with [`no_float`]) | `f32`, `f64` _(default)_ | `"f32"` or `"f64"` | `"123.4567"` etc. | | **Boolean value** | `bool` | `"bool"` | `"true"` or `"false"` | | **Unicode character** | `char` | `"char"` | `"A"`, `"x"` etc. | -| **Immutable Unicode string** | `rhai::ImmutableString` (implemented as `Rc` or `Arc`, _not_ `&str`) | `"string"` | `"hello"` etc. | +| **Immutable Unicode string** | `rhai::ImmutableString` (implemented as `Rc` or `Arc`) | `"string"` | `"hello"` etc. | | **Array** (disabled with [`no_index`]) | `rhai::Array` | `"array"` | `"[ ?, ?, ? ]"` | | **Object map** (disabled with [`no_object`]) | `rhai::Map` | `"map"` | `#{ "a": 1, "b": 2 }` | | **Timestamp** (implemented in the [`BasicTimePackage`](#packages)) | `std::time::Instant` | `"timestamp"` | _not supported_ | @@ -1039,13 +1039,13 @@ struct TestStruct { field: String } -// Remember Rhai uses 'ImmutableString' instead of 'String' impl TestStruct { - fn get_field(&mut self) -> ImmutableString { - // Make an 'ImmutableString' from a 'String' - self.field.into(0) + // Returning a 'String' is OK - Rhai converts it into 'ImmutableString' + fn get_field(&mut self) -> String { + self.field.clone() } + // Remember Rhai uses 'ImmutableString' or '&str' instead of 'String' fn set_field(&mut self, new_val: ImmutableString) { // Get a 'String' from an 'ImmutableString' self.field = (*new_val).clone(); diff --git a/RELEASES.md b/RELEASES.md index 3622167a..011c5c80 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,9 +1,12 @@ Rhai Release Notes ================== -Version 0.16.0 +Version 0.15.1 ============== +This is a minor release which enables updating indexers (via registered indexer setters) and supports functions +with `&str` parameters (maps transparently to `ImmutableString`). + Breaking changes ---------------- @@ -14,13 +17,15 @@ Breaking changes New features ------------ -* Indexers are now split into getters ans setters (which now support updates). The API is split into `Engine::register_indexer_get` and `Engine::register_indexer_set` with `Engine::register_indexer_get_set` being a shorthand. Similarly, `Module::set_indexer_get_fn` and `Module::set_indexer_set_fn` are added. +* Indexers are now split into getters and setters (which now support updates). The API is split into `Engine::register_indexer_get` and `Engine::register_indexer_set` with `Engine::register_indexer_get_set` being a shorthand. Similarly, `Module::set_indexer_get_fn` and `Module::set_indexer_set_fn` are added. * `Engine:register_fn` and `Engine:register_result_fn` accepts functions that take parameters of type `&str` (immutable string slice), which maps directly to `ImmutableString`. This is to avoid needing wrappers for functions taking string parameters. Version 0.15.0 ============== +This version uses immutable strings (`ImmutableString` type) and built-in operator functions (e.g. `+`, `>`, `+=`) to improve speed, plus some bug fixes. + Regression fix -------------- diff --git a/examples/strings.rs b/examples/strings.rs index 9e874377..e9d53016 100644 --- a/examples/strings.rs +++ b/examples/strings.rs @@ -74,6 +74,4 @@ fn main() -> Result<(), Box> { println!(); } - - Ok(()) } From 0ac3a7d8b48f2201710ed67cbdb15fae28c29bdc Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 11 Jun 2020 18:13:33 +0800 Subject: [PATCH 24/53] Use type alias TokenStream. --- src/engine.rs | 48 ++++++++++----------- src/module.rs | 20 ++++----- src/parser.rs | 117 ++++++++++++++++++++++++-------------------------- src/token.rs | 2 + 4 files changed, 93 insertions(+), 94 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 0408d50f..8c860b7f 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -200,8 +200,8 @@ pub fn get_script_function_by_signature<'a>( public_only: bool, ) -> Option<&'a ScriptFnDef> { // Qualifiers (none) + function name + number of arguments. - let hash_fn_def = calc_fn_hash(empty(), name, params, empty()); - let func = module.get_fn(hash_fn_def)?; + let hash_script = calc_fn_hash(empty(), name, params, empty()); + let func = module.get_fn(hash_script)?; if !func.is_script() { return None; } @@ -524,7 +524,7 @@ impl Engine { state: &mut State, lib: &Module, fn_name: &str, - hashes: (u64, u64), + (hash_fn, hash_script): (u64, u64), args: &mut FnCallArgs, is_ref: bool, def_val: Option<&Dynamic>, @@ -532,7 +532,7 @@ impl Engine { ) -> Result<(Dynamic, bool), Box> { self.inc_operations(state)?; - let native_only = hashes.1 == 0; + let native_only = hash_script == 0; // Check for stack overflow #[cfg(not(feature = "no_function"))] @@ -587,14 +587,14 @@ impl Engine { // Then search packages // NOTE: We skip script functions for global_module and packages, and native functions for lib let func = if !native_only { - lib.get_fn(hashes.1) //.or_else(|| lib.get_fn(hashes.0)) + lib.get_fn(hash_script) //.or_else(|| lib.get_fn(hash_fn)) } else { None } - //.or_else(|| self.global_module.get_fn(hashes.1)) - .or_else(|| self.global_module.get_fn(hashes.0)) - //.or_else(|| self.packages.get_fn(hashes.1)) - .or_else(|| self.packages.get_fn(hashes.0)); + //.or_else(|| self.global_module.get_fn(hash_script)) + .or_else(|| self.global_module.get_fn(hash_fn)) + //.or_else(|| self.packages.get_fn(hash_script)) + .or_else(|| self.packages.get_fn(hash_fn)); if let Some(func) = func { // Calling pure function in method-call? @@ -784,18 +784,18 @@ impl Engine { } // Has a system function an override? - fn has_override(&self, lib: &Module, hashes: (u64, u64)) -> bool { + fn has_override(&self, lib: &Module, (hash_fn, hash_script): (u64, u64)) -> bool { // NOTE: We skip script functions for global_module and packages, and native functions for lib // First check script-defined functions - lib.contains_fn(hashes.1) - //|| lib.contains_fn(hashes.0) + lib.contains_fn(hash_script) + //|| lib.contains_fn(hash_fn) // Then check registered functions - //|| self.global_module.contains_fn(hashes.1) - || self.global_module.contains_fn(hashes.0) + //|| self.global_module.contains_fn(hash_script) + || self.global_module.contains_fn(hash_fn) // Then check packages - //|| self.packages.contains_fn(hashes.1) - || self.packages.contains_fn(hashes.0) + //|| self.packages.contains_fn(hash_script) + || self.packages.contains_fn(hash_fn) } /// Perform an actual function call, taking care of special functions @@ -812,7 +812,7 @@ impl Engine { lib: &Module, fn_name: &str, native_only: bool, - hash_fn_def: u64, + hash_script: u64, args: &mut FnCallArgs, is_ref: bool, def_val: Option<&Dynamic>, @@ -821,7 +821,7 @@ impl Engine { // Qualifiers (none) + function name + number of arguments + argument `TypeId`'s. let arg_types = args.iter().map(|a| a.type_id()); let hash_fn = calc_fn_hash(empty(), fn_name, args.len(), arg_types); - let hashes = (hash_fn, if native_only { 0 } else { hash_fn_def }); + let hashes = (hash_fn, if native_only { 0 } else { hash_script }); match fn_name { // type_of @@ -1412,7 +1412,7 @@ impl Engine { Expr::Property(_) => unreachable!(), // Statement block - Expr::Stmt(stmt) => self.eval_stmt(scope, state, lib, &stmt.0, level), + Expr::Stmt(x) => self.eval_stmt(scope, state, lib, &x.0, level), // var op= rhs Expr::Assignment(x) if matches!(x.0, Expr::Variable(_)) => { @@ -1625,7 +1625,7 @@ impl Engine { // Module-qualified function call Expr::FnCall(x) if x.1.is_some() => { - let ((name, _, pos), modules, hash_fn_def, args_expr, def_val) = x.as_ref(); + let ((name, _, pos), modules, hash_script, args_expr, def_val) = x.as_ref(); let modules = modules.as_ref().unwrap(); let mut arg_values = args_expr @@ -1650,13 +1650,13 @@ impl Engine { }; // First search in script-defined functions (can override built-in) - let func = match module.get_qualified_fn(name, *hash_fn_def) { + let func = match module.get_qualified_fn(name, *hash_script) { Err(err) if matches!(*err, EvalAltResult::ErrorFunctionNotFound(_, _)) => { // Then search in Rust functions self.inc_operations(state) .map_err(|err| EvalAltResult::new_position(err, *pos))?; - // Rust functions are indexed in two steps: + // Qualified Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, // i.e. qualifiers + function name + number of arguments. // 2) Calculate a second hash with no qualifiers, empty function name, @@ -1664,9 +1664,9 @@ impl Engine { let hash_fn_args = calc_fn_hash(empty(), "", 0, args.iter().map(|a| a.type_id())); // 3) The final hash is the XOR of the two hashes. - let hash_fn_native = *hash_fn_def ^ hash_fn_args; + let hash_qualified_fn = *hash_script ^ hash_fn_args; - module.get_qualified_fn(name, hash_fn_native) + module.get_qualified_fn(name, hash_qualified_fn) } r => r, }; diff --git a/src/module.rs b/src/module.rs index 1deddee4..e607a3e4 100644 --- a/src/module.rs +++ b/src/module.rs @@ -187,9 +187,9 @@ impl Module { /// If there is an existing function of the same name and number of arguments, it is replaced. pub(crate) fn set_script_fn(&mut self, fn_def: ScriptFnDef) { // None + function name + number of arguments. - let hash_fn_def = calc_fn_hash(empty(), &fn_def.name, fn_def.params.len(), empty()); + let hash_script = calc_fn_hash(empty(), &fn_def.name, fn_def.params.len(), empty()); self.functions.insert( - hash_fn_def, + hash_script, ( fn_def.name.to_string(), fn_def.access, @@ -778,9 +778,9 @@ impl Module { pub(crate) fn get_qualified_fn( &mut self, name: &str, - hash_fn_native: u64, + hash_qualified_fn: u64, ) -> Result<&CallableFunction, Box> { - self.all_functions.get(&hash_fn_native).ok_or_else(|| { + self.all_functions.get(&hash_qualified_fn).ok_or_else(|| { Box::new(EvalAltResult::ErrorFunctionNotFound( name.to_string(), Position::none(), @@ -907,26 +907,26 @@ impl Module { if func.is_script() { let fn_def = func.get_shared_fn_def(); // Qualifiers + function name + number of arguments. - let hash_fn_def = calc_fn_hash( + let hash_qualified_script = calc_fn_hash( qualifiers.iter().map(|&v| v), &fn_def.name, fn_def.params.len(), empty(), ); - functions.push((hash_fn_def, fn_def.into())); + functions.push((hash_qualified_script, fn_def.into())); } else { - // Rust functions are indexed in two steps: + // Qualified Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, // i.e. qualifiers + function name + number of arguments. - let hash_fn_def = + let hash_qualified_script = calc_fn_hash(qualifiers.iter().map(|&v| v), name, params.len(), empty()); // 2) Calculate a second hash with no qualifiers, empty function name, // zero number of arguments, and the actual list of argument `TypeId`'.s let hash_fn_args = calc_fn_hash(empty(), "", 0, params.iter().cloned()); // 3) The final hash is the XOR of the two hashes. - let hash_fn_native = hash_fn_def ^ hash_fn_args; + let hash_qualified_fn = hash_qualified_script ^ hash_fn_args; - functions.push((hash_fn_native, func.clone())); + functions.push((hash_qualified_fn, func.clone())); } } } diff --git a/src/parser.rs b/src/parser.rs index ab8de95c..efdae44d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -7,7 +7,7 @@ use crate::error::{LexError, ParseError, ParseErrorType}; use crate::module::{Module, ModuleRef}; use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::scope::{EntryType as ScopeEntryType, Scope}; -use crate::token::{Position, Token, TokenIterator}; +use crate::token::{Position, Token, TokenStream}; use crate::utils::{StaticVec, StraightHasherBuilder}; use crate::stdlib::{ @@ -672,7 +672,7 @@ impl Expr { } /// Consume a particular token, checking that it is the expected one. -fn eat_token(input: &mut Peekable, token: Token) -> Position { +fn eat_token(input: &mut TokenStream, token: Token) -> Position { let (t, pos) = input.next().unwrap(); if t != token { @@ -687,7 +687,7 @@ fn eat_token(input: &mut Peekable, token: Token) -> Position { } /// Match a particular token, consuming it if matched. -fn match_token(input: &mut Peekable, token: Token) -> Result { +fn match_token(input: &mut TokenStream, token: Token) -> Result { let (t, _) = input.peek().unwrap(); if *t == token { eat_token(input, token); @@ -698,8 +698,8 @@ fn match_token(input: &mut Peekable, token: Token) -> Result( - input: &mut Peekable>, +fn parse_paren_expr( + input: &mut TokenStream, state: &mut ParseState, pos: Position, level: usize, @@ -731,8 +731,8 @@ fn parse_paren_expr<'a>( } /// Parse a function call. -fn parse_call_expr<'a>( - input: &mut Peekable>, +fn parse_call_expr( + input: &mut TokenStream, state: &mut ParseState, id: String, mut modules: Option>, @@ -764,7 +764,7 @@ fn parse_call_expr<'a>( Token::RightParen => { eat_token(input, Token::RightParen); - let hash_fn_def = if let Some(modules) = modules.as_mut() { + let hash_script = if let Some(modules) = modules.as_mut() { modules.set_index(state.find_module(&modules.get(0).0)); // Rust functions are indexed in two steps: @@ -783,7 +783,7 @@ fn parse_call_expr<'a>( return Ok(Expr::FnCall(Box::new(( (id.into(), false, begin), modules, - hash_fn_def, + hash_script, args, None, )))); @@ -800,7 +800,7 @@ fn parse_call_expr<'a>( (Token::RightParen, _) => { eat_token(input, Token::RightParen); - let hash_fn_def = if let Some(modules) = modules.as_mut() { + let hash_script = if let Some(modules) = modules.as_mut() { modules.set_index(state.find_module(&modules.get(0).0)); // Rust functions are indexed in two steps: @@ -819,7 +819,7 @@ fn parse_call_expr<'a>( return Ok(Expr::FnCall(Box::new(( (id.into(), false, begin), modules, - hash_fn_def, + hash_script, args, None, )))); @@ -854,8 +854,8 @@ fn parse_call_expr<'a>( /// Parse an indexing chain. /// Indexing binds to the right, so this call parses all possible levels of indexing following in the input. -fn parse_index_chain<'a>( - input: &mut Peekable>, +fn parse_index_chain( + input: &mut TokenStream, state: &mut ParseState, lhs: Expr, pos: Position, @@ -1044,8 +1044,8 @@ fn parse_index_chain<'a>( } /// Parse an array literal. -fn parse_array_literal<'a>( - input: &mut Peekable>, +fn parse_array_literal( + input: &mut TokenStream, state: &mut ParseState, pos: Position, level: usize, @@ -1094,8 +1094,8 @@ fn parse_array_literal<'a>( } /// Parse a map literal. -fn parse_map_literal<'a>( - input: &mut Peekable>, +fn parse_map_literal( + input: &mut TokenStream, state: &mut ParseState, pos: Position, level: usize, @@ -1197,8 +1197,8 @@ fn parse_map_literal<'a>( } /// Parse a primary expression. -fn parse_primary<'a>( - input: &mut Peekable>, +fn parse_primary( + input: &mut TokenStream, state: &mut ParseState, level: usize, if_expr: bool, @@ -1319,8 +1319,8 @@ fn parse_primary<'a>( } /// Parse a potential unary operator. -fn parse_unary<'a>( - input: &mut Peekable>, +fn parse_unary( + input: &mut TokenStream, state: &mut ParseState, level: usize, if_expr: bool, @@ -1470,8 +1470,8 @@ fn make_assignment_stmt<'a>( } /// Parse an operator-assignment expression. -fn parse_op_assignment_stmt<'a>( - input: &mut Peekable>, +fn parse_op_assignment_stmt( + input: &mut TokenStream, state: &mut ParseState, lhs: Expr, level: usize, @@ -1713,8 +1713,8 @@ fn make_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result( - input: &mut Peekable>, +fn parse_binary_op( + input: &mut TokenStream, state: &mut ParseState, parent_precedence: u8, lhs: Expr, @@ -1832,8 +1832,8 @@ fn parse_binary_op<'a>( } /// Parse an expression. -fn parse_expr<'a>( - input: &mut Peekable>, +fn parse_expr( + input: &mut TokenStream, state: &mut ParseState, level: usize, if_expr: bool, @@ -1850,10 +1850,7 @@ fn parse_expr<'a>( } /// Make sure that the expression is not a statement expression (i.e. wrapped in `{}`). -fn ensure_not_statement_expr<'a>( - input: &mut Peekable>, - type_name: &str, -) -> Result<(), ParseError> { +fn ensure_not_statement_expr(input: &mut TokenStream, type_name: &str) -> Result<(), ParseError> { match input.peek().unwrap() { // Disallow statement expressions (Token::LeftBrace, pos) | (Token::EOF, pos) => { @@ -1865,7 +1862,7 @@ fn ensure_not_statement_expr<'a>( } /// Make sure that the expression is not a mis-typed assignment (i.e. `a = b` instead of `a == b`). -fn ensure_not_assignment<'a>(input: &mut Peekable>) -> Result<(), ParseError> { +fn ensure_not_assignment(input: &mut TokenStream) -> Result<(), ParseError> { match input.peek().unwrap() { (Token::Equals, pos) => { return Err(PERR::BadInput("Possibly a typo of '=='?".to_string()).into_err(*pos)) @@ -1892,8 +1889,8 @@ fn ensure_not_assignment<'a>(input: &mut Peekable>) -> Result< } /// Parse an if statement. -fn parse_if<'a>( - input: &mut Peekable>, +fn parse_if( + input: &mut TokenStream, state: &mut ParseState, breakable: bool, level: usize, @@ -1930,8 +1927,8 @@ fn parse_if<'a>( } /// Parse a while loop. -fn parse_while<'a>( - input: &mut Peekable>, +fn parse_while( + input: &mut TokenStream, state: &mut ParseState, level: usize, if_expr: bool, @@ -1954,8 +1951,8 @@ fn parse_while<'a>( } /// Parse a loop statement. -fn parse_loop<'a>( - input: &mut Peekable>, +fn parse_loop( + input: &mut TokenStream, state: &mut ParseState, level: usize, if_expr: bool, @@ -1975,8 +1972,8 @@ fn parse_loop<'a>( } /// Parse a for loop. -fn parse_for<'a>( - input: &mut Peekable>, +fn parse_for( + input: &mut TokenStream, state: &mut ParseState, level: usize, if_expr: bool, @@ -2028,8 +2025,8 @@ fn parse_for<'a>( } /// Parse a variable definition statement. -fn parse_let<'a>( - input: &mut Peekable>, +fn parse_let( + input: &mut TokenStream, state: &mut ParseState, var_type: ScopeEntryType, level: usize, @@ -2091,8 +2088,8 @@ fn parse_let<'a>( } /// Parse an import statement. -fn parse_import<'a>( - input: &mut Peekable>, +fn parse_import( + input: &mut TokenStream, state: &mut ParseState, level: usize, if_expr: bool, @@ -2132,8 +2129,8 @@ fn parse_import<'a>( /// Parse an export statement. #[cfg(not(feature = "no_module"))] -fn parse_export<'a>( - input: &mut Peekable>, +fn parse_export( + input: &mut TokenStream, state: &mut ParseState, level: usize, ) -> Result { @@ -2197,8 +2194,8 @@ fn parse_export<'a>( } /// Parse a statement block. -fn parse_block<'a>( - input: &mut Peekable>, +fn parse_block( + input: &mut TokenStream, state: &mut ParseState, breakable: bool, level: usize, @@ -2278,8 +2275,8 @@ fn parse_block<'a>( } /// Parse an expression as a statement. -fn parse_expr_stmt<'a>( - input: &mut Peekable>, +fn parse_expr_stmt( + input: &mut TokenStream, state: &mut ParseState, level: usize, if_expr: bool, @@ -2297,8 +2294,8 @@ fn parse_expr_stmt<'a>( } /// Parse a single statement. -fn parse_stmt<'a>( - input: &mut Peekable>, +fn parse_stmt( + input: &mut TokenStream, state: &mut ParseState, breakable: bool, is_global: bool, @@ -2389,8 +2386,8 @@ fn parse_stmt<'a>( /// Parse a function definition. #[cfg(not(feature = "no_function"))] -fn parse_fn<'a>( - input: &mut Peekable>, +fn parse_fn( + input: &mut TokenStream, state: &mut ParseState, access: FnAccess, level: usize, @@ -2482,8 +2479,8 @@ fn parse_fn<'a>( } /// Parse the global level statements. -fn parse_global_level<'a>( - input: &mut Peekable>, +fn parse_global_level( + input: &mut TokenStream, max_expr_depth: usize, max_function_expr_depth: usize, ) -> Result<(Vec, Vec), ParseError> { @@ -2561,9 +2558,9 @@ fn parse_global_level<'a>( } impl Engine { - pub(crate) fn parse_global_expr<'a>( + pub(crate) fn parse_global_expr( &self, - input: &mut Peekable>, + input: &mut TokenStream, scope: &Scope, optimization_level: OptimizationLevel, ) -> Result { @@ -2589,9 +2586,9 @@ impl Engine { } /// Run the parser on an input stream, returning an AST. - pub(crate) fn parse<'a>( + pub(crate) fn parse( &self, - input: &mut Peekable>, + input: &mut TokenStream, scope: &Scope, optimization_level: OptimizationLevel, ) -> Result { diff --git a/src/token.rs b/src/token.rs index 2967fc02..d6799a6f 100644 --- a/src/token.rs +++ b/src/token.rs @@ -19,6 +19,8 @@ use crate::stdlib::{ type LERR = LexError; +pub type TokenStream<'a> = Peekable>; + /// A location (line number + character position) in the input script. /// /// In order to keep footprint small, both line number and character position have 16-bit unsigned resolution, From 8f55a15ab080df8c267db2be9af4fa1a1501c0b9 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 11 Jun 2020 22:03:47 +0800 Subject: [PATCH 25/53] Collect parse settings into struct type. --- src/parser.rs | 529 ++++++++++++++++++++++---------------------------- 1 file changed, 236 insertions(+), 293 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index efdae44d..0cd50b57 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -195,10 +195,11 @@ pub enum ReturnType { Exception, } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] struct ParseState { /// Encapsulates a local stack with variable names to simulate an actual runtime scope. stack: Vec<(String, ScopeEntryType)>, + /// Maximum levels of expression nesting. max_expr_depth: usize, } @@ -256,6 +257,41 @@ impl DerefMut for ParseState { } } +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +/// A type that encapsulates all the settings for a particular parsing function. +struct ParseSettings { + /// Current position. + pos: Position, + /// Is the construct being parsed located at global level? + is_global: bool, + /// Is the current position inside a loop? + is_breakable: bool, + /// Is if-expression allowed? + allow_if_expr: bool, + /// Is statement-expression allowed? + allow_stmt_expr: bool, + /// Current expression nesting level. + level: usize, +} + +impl ParseSettings { + /// Create a new `ParseSettings` with one higher expression level. + pub fn level_up(&self) -> Self { + Self { + level: self.level + 1, + ..*self + } + } + /// Make sure that the current level of expression nesting is within the maximum limit. + pub fn ensure_level_within_max_limit(&self, limit: usize) -> Result<(), ParseError> { + if self.level > limit { + Err(PERR::ExprTooDeep.into_err(self.pos)) + } else { + Ok(()) + } + } +} + /// A statement. /// /// Each variant is at most one pointer in size (for speed), @@ -701,32 +737,29 @@ fn match_token(input: &mut TokenStream, token: Token) -> Result Result { - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.ensure_level_within_max_limit(state.max_expr_depth)?; if match_token(input, Token::RightParen)? { - return Ok(Expr::Unit(pos)); + return Ok(Expr::Unit(settings.pos)); } - let expr = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; + let expr = parse_expr(input, state, settings.level_up())?; match input.next().unwrap() { // ( xxx ) (Token::RightParen, _) => Ok(expr), // ( - (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), + (Token::LexError(err), pos) => { + return Err(PERR::BadInput(err.to_string()).into_err(settings.pos)) + } // ( xxx ??? (_, pos) => Err(PERR::MissingToken( Token::RightParen.into(), "for a matching ( in this expression".into(), ) - .into_err(pos)), + .into_err(settings.pos)), } } @@ -736,16 +769,11 @@ fn parse_call_expr( state: &mut ParseState, id: String, mut modules: Option>, - begin: Position, - level: usize, - if_expr: bool, - stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { - let (token, pos) = input.peek().unwrap(); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(*pos)); - } + let (token, token_pos) = input.peek().unwrap(); + settings.pos = *token_pos; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let mut args = StaticVec::new(); @@ -756,10 +784,10 @@ fn parse_call_expr( Token::RightParen.into(), format!("to close the arguments list of this function call '{}'", id), ) - .into_err(*pos)) + .into_err(settings.pos)) } // id - Token::LexError(err) => return Err(PERR::BadInput(err.to_string()).into_err(*pos)), + Token::LexError(err) => return Err(PERR::BadInput(err.to_string()).into_err(settings.pos)), // id() Token::RightParen => { eat_token(input, Token::RightParen); @@ -781,7 +809,7 @@ fn parse_call_expr( }; return Ok(Expr::FnCall(Box::new(( - (id.into(), false, begin), + (id.into(), false, settings.pos), modules, hash_script, args, @@ -792,8 +820,10 @@ fn parse_call_expr( _ => (), } + let settings = settings.level_up(); + loop { - args.push(parse_expr(input, state, level + 1, if_expr, stmt_expr)?); + args.push(parse_expr(input, state, settings)?); match input.peek().unwrap() { // id(...args) @@ -817,7 +847,7 @@ fn parse_call_expr( }; return Ok(Expr::FnCall(Box::new(( - (id.into(), false, begin), + (id.into(), false, settings.pos), modules, hash_script, args, @@ -858,16 +888,11 @@ fn parse_index_chain( input: &mut TokenStream, state: &mut ParseState, lhs: Expr, - pos: Position, - level: usize, - if_expr: bool, - stmt_expr: bool, + settings: ParseSettings, ) -> Result { - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.ensure_level_within_max_limit(state.max_expr_depth)?; - let idx_expr = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; + let idx_expr = parse_expr(input, state, settings.level_up())?; // Check type of indexing - must be integer or string match &idx_expr { @@ -1008,17 +1033,9 @@ fn parse_index_chain( (Token::LeftBracket, _) => { let idx_pos = eat_token(input, Token::LeftBracket); // Recursively parse the indexing chain, right-binding each - let idx_expr = parse_index_chain( - input, - state, - idx_expr, - idx_pos, - level + 1, - if_expr, - stmt_expr, - )?; + let idx_expr = parse_index_chain(input, state, idx_expr, settings.level_up())?; // Indexing binds to right - Ok(Expr::Index(Box::new((lhs, idx_expr, pos)))) + Ok(Expr::Index(Box::new((lhs, idx_expr, settings.pos)))) } // Otherwise terminate the indexing chain _ => { @@ -1027,9 +1044,9 @@ fn parse_index_chain( // inside brackets to be mis-parsed as another level of indexing, or a // dot expression/function call to be mis-parsed as following the indexing chain. Expr::Index(_) | Expr::Dot(_) | Expr::FnCall(_) => Ok(Expr::Index( - Box::new((lhs, Expr::Expr(Box::new(idx_expr)), pos)), + Box::new((lhs, Expr::Expr(Box::new(idx_expr)), settings.pos)), )), - _ => Ok(Expr::Index(Box::new((lhs, idx_expr, pos)))), + _ => Ok(Expr::Index(Box::new((lhs, idx_expr, settings.pos)))), } } } @@ -1047,20 +1064,15 @@ fn parse_index_chain( fn parse_array_literal( input: &mut TokenStream, state: &mut ParseState, - pos: Position, - level: usize, - if_expr: bool, - stmt_expr: bool, + settings: ParseSettings, ) -> Result { - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let mut arr = StaticVec::new(); if !match_token(input, Token::RightBracket)? { while !input.peek().unwrap().0.is_eof() { - let expr = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; + let expr = parse_expr(input, state, settings.level_up())?; arr.push(expr); match input.peek().unwrap() { @@ -1090,21 +1102,16 @@ fn parse_array_literal( } } - Ok(Expr::Array(Box::new((arr, pos)))) + Ok(Expr::Array(Box::new((arr, settings.pos)))) } /// Parse a map literal. fn parse_map_literal( input: &mut TokenStream, state: &mut ParseState, - pos: Position, - level: usize, - if_expr: bool, - stmt_expr: bool, + settings: ParseSettings, ) -> Result { - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let mut map = StaticVec::new(); @@ -1150,7 +1157,7 @@ fn parse_map_literal( } }; - let expr = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; + let expr = parse_expr(input, state, settings.level_up())?; map.push(((name, pos), expr)); @@ -1193,56 +1200,51 @@ fn parse_map_literal( }) .map_err(|(key, pos)| PERR::DuplicatedProperty(key.to_string()).into_err(pos))?; - Ok(Expr::Map(Box::new((map, pos)))) + Ok(Expr::Map(Box::new((map, settings.pos)))) } /// Parse a primary expression. fn parse_primary( input: &mut TokenStream, state: &mut ParseState, - level: usize, - if_expr: bool, - stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { - let (token, pos) = input.peek().unwrap(); - let pos = *pos; - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + let (token, pos1) = input.peek().unwrap(); + settings.pos = *pos1; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let (token, _) = match token { // { - block statement as expression - Token::LeftBrace if stmt_expr => { - return parse_block(input, state, false, level + 1, if_expr, stmt_expr) - .map(|block| Expr::Stmt(Box::new((block, pos)))); + Token::LeftBrace if settings.allow_stmt_expr => { + return parse_block(input, state, settings.level_up()) + .map(|block| Expr::Stmt(Box::new((block, settings.pos)))) } - Token::EOF => return Err(PERR::UnexpectedEOF.into_err(pos)), + Token::EOF => return Err(PERR::UnexpectedEOF.into_err(settings.pos)), _ => input.next().unwrap(), }; let mut root_expr = match token { - Token::IntegerConstant(x) => Expr::IntegerConstant(Box::new((x, pos))), + Token::IntegerConstant(x) => Expr::IntegerConstant(Box::new((x, settings.pos))), #[cfg(not(feature = "no_float"))] - Token::FloatConstant(x) => Expr::FloatConstant(Box::new((x, pos))), - Token::CharConstant(c) => Expr::CharConstant(Box::new((c, pos))), - Token::StringConst(s) => Expr::StringConstant(Box::new((s.into(), pos))), + Token::FloatConstant(x) => Expr::FloatConstant(Box::new((x, settings.pos))), + Token::CharConstant(c) => Expr::CharConstant(Box::new((c, settings.pos))), + Token::StringConst(s) => Expr::StringConstant(Box::new((s.into(), settings.pos))), Token::Identifier(s) => { let index = state.find(&s); - Expr::Variable(Box::new(((s, pos), None, 0, index))) + Expr::Variable(Box::new(((s, settings.pos), None, 0, index))) } - Token::LeftParen => parse_paren_expr(input, state, pos, level + 1, if_expr, stmt_expr)?, + Token::LeftParen => parse_paren_expr(input, state, settings.level_up())?, #[cfg(not(feature = "no_index"))] - Token::LeftBracket => { - parse_array_literal(input, state, pos, level + 1, if_expr, stmt_expr)? - } + Token::LeftBracket => parse_array_literal(input, state, settings.level_up())?, #[cfg(not(feature = "no_object"))] - Token::MapStart => parse_map_literal(input, state, pos, level + 1, if_expr, stmt_expr)?, - Token::True => Expr::True(pos), - Token::False => Expr::False(pos), - Token::LexError(err) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), + Token::MapStart => parse_map_literal(input, state, settings.level_up())?, + Token::True => Expr::True(settings.pos), + Token::False => Expr::False(settings.pos), + Token::LexError(err) => return Err(PERR::BadInput(err.to_string()).into_err(settings.pos)), token => { - return Err(PERR::BadInput(format!("Unexpected '{}'", token.syntax())).into_err(pos)) + return Err( + PERR::BadInput(format!("Unexpected '{}'", token.syntax())).into_err(settings.pos) + ) } }; @@ -1260,16 +1262,7 @@ fn parse_primary( // Function call (Expr::Variable(x), Token::LeftParen) => { let ((name, pos), modules, _, _) = *x; - parse_call_expr( - input, - state, - name, - modules, - pos, - level + 1, - if_expr, - stmt_expr, - )? + parse_call_expr(input, state, name, modules, settings.level_up())? } (Expr::Property(_), _) => unreachable!(), // module access @@ -1291,7 +1284,8 @@ fn parse_primary( // Indexing #[cfg(not(feature = "no_index"))] (expr, Token::LeftBracket) => { - parse_index_chain(input, state, expr, token_pos, level + 1, if_expr, stmt_expr)? + settings.pos = token_pos; + parse_index_chain(input, state, expr, settings.level_up())? } // Unknown postfix operator (expr, token) => panic!( @@ -1322,28 +1316,23 @@ fn parse_primary( fn parse_unary( input: &mut TokenStream, state: &mut ParseState, - level: usize, - if_expr: bool, - stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { - let (token, pos) = input.peek().unwrap(); - let pos = *pos; - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + let (token, token_pos) = input.peek().unwrap(); + settings.pos = *token_pos; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; match token { // If statement is allowed to act as expressions - Token::If if if_expr => Ok(Expr::Stmt(Box::new(( - parse_if(input, state, false, level + 1, if_expr, stmt_expr)?, - pos, + Token::If if settings.allow_if_expr => Ok(Expr::Stmt(Box::new(( + parse_if(input, state, settings.level_up())?, + settings.pos, )))), // -expr Token::UnaryMinus => { let pos = eat_token(input, Token::UnaryMinus); - match parse_unary(input, state, level + 1, if_expr, stmt_expr)? { + match parse_unary(input, state, settings.level_up())? { // Negative integer Expr::IntegerConstant(x) => { let (num, pos) = *x; @@ -1392,13 +1381,13 @@ fn parse_unary( // +expr Token::UnaryPlus => { eat_token(input, Token::UnaryPlus); - parse_unary(input, state, level + 1, if_expr, stmt_expr) + parse_unary(input, state, settings.level_up()) } // !expr Token::Bang => { let pos = eat_token(input, Token::Bang); let mut args = StaticVec::new(); - let expr = parse_primary(input, state, level + 1, if_expr, stmt_expr)?; + let expr = parse_primary(input, state, settings.level_up())?; args.push(expr); let op = "!"; @@ -1413,9 +1402,9 @@ fn parse_unary( )))) } // - Token::EOF => Err(PERR::UnexpectedEOF.into_err(pos)), + Token::EOF => Err(PERR::UnexpectedEOF.into_err(settings.pos)), // All other tokens - _ => parse_primary(input, state, level + 1, if_expr, stmt_expr), + _ => parse_primary(input, state, settings.level_up()), } } @@ -1474,16 +1463,11 @@ fn parse_op_assignment_stmt( input: &mut TokenStream, state: &mut ParseState, lhs: Expr, - level: usize, - if_expr: bool, - stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { - let (token, pos) = input.peek().unwrap(); - let pos = *pos; - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + let (token, token_pos) = input.peek().unwrap(); + settings.pos = *token_pos; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let op = match token { Token::Equals => "".into(), @@ -1504,7 +1488,7 @@ fn parse_op_assignment_stmt( }; let (_, pos) = input.next().unwrap(); - let rhs = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; + let rhs = parse_expr(input, state, settings.level_up())?; make_assignment_stmt(op, state, lhs, rhs, pos) } @@ -1718,13 +1702,10 @@ fn parse_binary_op( state: &mut ParseState, parent_precedence: u8, lhs: Expr, - mut level: usize, - if_expr: bool, - stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(lhs.position())); - } + settings.pos = lhs.position(); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let mut root = lhs; @@ -1741,24 +1722,22 @@ fn parse_binary_op( let (op_token, pos) = input.next().unwrap(); - let rhs = parse_unary(input, state, level, if_expr, stmt_expr)?; + let rhs = parse_unary(input, state, settings)?; let next_precedence = input.peek().unwrap().0.precedence(); // Bind to right if the next operator has higher precedence // If same precedence, then check if the operator binds right let rhs = if (precedence == next_precedence && bind_right) || precedence < next_precedence { - parse_binary_op(input, state, precedence, rhs, level, if_expr, stmt_expr)? + parse_binary_op(input, state, precedence, rhs, settings)? } else { // Otherwise bind to left (even if next operator has the same precedence) rhs }; - level += 1; - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings = settings.level_up(); + settings.pos = pos; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let cmp_def = Some(false.into()); let op = op_token.syntax(); @@ -1835,18 +1814,13 @@ fn parse_binary_op( fn parse_expr( input: &mut TokenStream, state: &mut ParseState, - level: usize, - if_expr: bool, - stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { - let (_, pos) = input.peek().unwrap(); + settings.pos = input.peek().unwrap().1; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(*pos)); - } - - let lhs = parse_unary(input, state, level + 1, if_expr, stmt_expr)?; - parse_binary_op(input, state, 1, lhs, level + 1, if_expr, stmt_expr) + let lhs = parse_unary(input, state, settings.level_up())?; + parse_binary_op(input, state, 1, lhs, settings.level_up()) } /// Make sure that the expression is not a statement expression (i.e. wrapped in `{}`). @@ -1892,32 +1866,26 @@ fn ensure_not_assignment(input: &mut TokenStream) -> Result<(), ParseError> { fn parse_if( input: &mut TokenStream, state: &mut ParseState, - breakable: bool, - level: usize, - if_expr: bool, - stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { // if ... - let pos = eat_token(input, Token::If); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.pos = eat_token(input, Token::If); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; // if guard { if_body } ensure_not_statement_expr(input, "a boolean")?; - let guard = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; + let guard = parse_expr(input, state, settings.level_up())?; ensure_not_assignment(input)?; - let if_body = parse_block(input, state, breakable, level + 1, if_expr, stmt_expr)?; + let if_body = parse_block(input, state, settings.level_up())?; // if guard { if_body } else ... let else_body = if match_token(input, Token::Else).unwrap_or(false) { Some(if let (Token::If, _) = input.peek().unwrap() { // if guard { if_body } else if ... - parse_if(input, state, breakable, level + 1, if_expr, stmt_expr)? + parse_if(input, state, settings.level_up())? } else { // if guard { if_body } else { else-body } - parse_block(input, state, breakable, level + 1, if_expr, stmt_expr)? + parse_block(input, state, settings.level_up())? }) } else { None @@ -1930,22 +1898,19 @@ fn parse_if( fn parse_while( input: &mut TokenStream, state: &mut ParseState, - level: usize, - if_expr: bool, - stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { // while ... - let pos = eat_token(input, Token::While); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.pos = eat_token(input, Token::While); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; // while guard { body } ensure_not_statement_expr(input, "a boolean")?; - let guard = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; + let guard = parse_expr(input, state, settings.level_up())?; ensure_not_assignment(input)?; - let body = parse_block(input, state, true, level + 1, if_expr, stmt_expr)?; + + settings.is_breakable = true; + let body = parse_block(input, state, settings.level_up())?; Ok(Stmt::While(Box::new((guard, body)))) } @@ -1954,19 +1919,15 @@ fn parse_while( fn parse_loop( input: &mut TokenStream, state: &mut ParseState, - level: usize, - if_expr: bool, - stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { // loop ... - let pos = eat_token(input, Token::Loop); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.pos = eat_token(input, Token::Loop); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; // loop { body } - let body = parse_block(input, state, true, level + 1, if_expr, stmt_expr)?; + settings.is_breakable = true; + let body = parse_block(input, state, settings.level_up())?; Ok(Stmt::Loop(Box::new(body))) } @@ -1975,16 +1936,11 @@ fn parse_loop( fn parse_for( input: &mut TokenStream, state: &mut ParseState, - level: usize, - if_expr: bool, - stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { // for ... - let pos = eat_token(input, Token::For); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.pos = eat_token(input, Token::For); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; // for name ... let name = match input.next().unwrap() { @@ -2012,12 +1968,13 @@ fn parse_for( // for name in expr { body } ensure_not_statement_expr(input, "a boolean")?; - let expr = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; + let expr = parse_expr(input, state, settings.level_up())?; let prev_len = state.len(); state.push((name.clone(), ScopeEntryType::Normal)); - let body = parse_block(input, state, true, level + 1, if_expr, stmt_expr)?; + settings.is_breakable = true; + let body = parse_block(input, state, settings.level_up())?; state.truncate(prev_len); @@ -2029,16 +1986,11 @@ fn parse_let( input: &mut TokenStream, state: &mut ParseState, var_type: ScopeEntryType, - level: usize, - if_expr: bool, - stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { // let/const... (specified in `var_type`) - let (_, pos) = input.next().unwrap(); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.pos = input.next().unwrap().1; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; // let name ... let (name, pos) = match input.next().unwrap() { @@ -2050,7 +2002,7 @@ fn parse_let( // let name = ... if match_token(input, Token::Equals)? { // let name = expr - let init_value = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; + let init_value = parse_expr(input, state, settings.level_up())?; match var_type { // let name = expr @@ -2091,19 +2043,14 @@ fn parse_let( fn parse_import( input: &mut TokenStream, state: &mut ParseState, - level: usize, - if_expr: bool, - stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { // import ... - let pos = eat_token(input, Token::Import); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.pos = eat_token(input, Token::Import); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; // import expr ... - let expr = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; + let expr = parse_expr(input, state, settings.level_up())?; // import expr as ... match input.next().unwrap() { @@ -2124,7 +2071,7 @@ fn parse_import( }; state.push((name.clone(), ScopeEntryType::Module)); - Ok(Stmt::Import(Box::new((expr, (name, pos))))) + Ok(Stmt::Import(Box::new((expr, (name, settings.pos))))) } /// Parse an export statement. @@ -2132,13 +2079,10 @@ fn parse_import( fn parse_export( input: &mut TokenStream, state: &mut ParseState, - level: usize, + mut settings: ParseSettings, ) -> Result { - let pos = eat_token(input, Token::Export); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.pos = eat_token(input, Token::Export); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let mut exports = StaticVec::new(); @@ -2197,13 +2141,10 @@ fn parse_export( fn parse_block( input: &mut TokenStream, state: &mut ParseState, - breakable: bool, - level: usize, - if_expr: bool, - stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { // Must start with { - let pos = match input.next().unwrap() { + settings.pos = match input.next().unwrap() { (Token::LeftBrace, pos) => pos, (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), (_, pos) => { @@ -2215,24 +2156,15 @@ fn parse_block( } }; - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let mut statements = StaticVec::new(); let prev_len = state.len(); while !match_token(input, Token::RightBrace)? { // Parse statements inside the block - let stmt = parse_stmt( - input, - state, - breakable, - false, - level + 1, - if_expr, - stmt_expr, - )?; + settings.is_global = false; + let stmt = parse_stmt(input, state, settings.level_up())?; // See if it needs a terminating semicolon let need_semicolon = !stmt.is_self_terminated(); @@ -2271,25 +2203,20 @@ fn parse_block( state.truncate(prev_len); - Ok(Stmt::Block(Box::new((statements, pos)))) + Ok(Stmt::Block(Box::new((statements, settings.pos)))) } /// Parse an expression as a statement. fn parse_expr_stmt( input: &mut TokenStream, state: &mut ParseState, - level: usize, - if_expr: bool, - stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { - let (_, pos) = input.peek().unwrap(); + settings.pos = input.peek().unwrap().1; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(*pos)); - } - - let expr = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; - let expr = parse_op_assignment_stmt(input, state, expr, level + 1, if_expr, stmt_expr)?; + let expr = parse_expr(input, state, settings.level_up())?; + let expr = parse_op_assignment_stmt(input, state, expr, settings.level_up())?; Ok(Stmt::Expr(Box::new(expr))) } @@ -2297,53 +2224,45 @@ fn parse_expr_stmt( fn parse_stmt( input: &mut TokenStream, state: &mut ParseState, - breakable: bool, - is_global: bool, - level: usize, - if_expr: bool, - stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { use ScopeEntryType::{Constant, Normal}; - let (token, pos) = match input.peek().unwrap() { + let (token, token_pos) = match input.peek().unwrap() { (Token::EOF, pos) => return Ok(Stmt::Noop(*pos)), x => x, }; - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(*pos)); - } + settings.pos = *token_pos; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; match token { // Semicolon - empty statement - Token::SemiColon => Ok(Stmt::Noop(*pos)), + Token::SemiColon => Ok(Stmt::Noop(settings.pos)), - Token::LeftBrace => parse_block(input, state, breakable, level + 1, if_expr, stmt_expr), + Token::LeftBrace => parse_block(input, state, settings.level_up()), // fn ... #[cfg(not(feature = "no_function"))] - Token::Fn if !is_global => Err(PERR::WrongFnDefinition.into_err(*pos)), + Token::Fn if !settings.is_global => Err(PERR::WrongFnDefinition.into_err(settings.pos)), #[cfg(not(feature = "no_function"))] Token::Fn => unreachable!(), - Token::If => parse_if(input, state, breakable, level + 1, if_expr, stmt_expr), - Token::While => parse_while(input, state, level + 1, if_expr, stmt_expr), - Token::Loop => parse_loop(input, state, level + 1, if_expr, stmt_expr), - Token::For => parse_for(input, state, level + 1, if_expr, stmt_expr), + Token::If => parse_if(input, state, settings.level_up()), + Token::While => parse_while(input, state, settings.level_up()), + Token::Loop => parse_loop(input, state, settings.level_up()), + Token::For => parse_for(input, state, settings.level_up()), - Token::Continue if breakable => { + Token::Continue if settings.is_breakable => { let pos = eat_token(input, Token::Continue); Ok(Stmt::Continue(pos)) } - Token::Break if breakable => { + Token::Break if settings.is_breakable => { let pos = eat_token(input, Token::Break); Ok(Stmt::Break(pos)) } - Token::Continue | Token::Break => Err(PERR::LoopBreak.into_err(*pos)), + Token::Continue | Token::Break => Err(PERR::LoopBreak.into_err(settings.pos)), Token::Return | Token::Throw => { - let pos = *pos; - let return_type = match input.next().unwrap() { (Token::Return, _) => ReturnType::Return, (Token::Throw, _) => ReturnType::Exception, @@ -2354,12 +2273,13 @@ fn parse_stmt( // `return`/`throw` at (Token::EOF, pos) => Ok(Stmt::ReturnWithVal(Box::new(((return_type, *pos), None)))), // `return;` or `throw;` - (Token::SemiColon, _) => { - Ok(Stmt::ReturnWithVal(Box::new(((return_type, pos), None)))) - } + (Token::SemiColon, _) => Ok(Stmt::ReturnWithVal(Box::new(( + (return_type, settings.pos), + None, + )))), // `return` or `throw` with expression (_, _) => { - let expr = parse_expr(input, state, level + 1, if_expr, stmt_expr)?; + let expr = parse_expr(input, state, settings.level_up())?; let pos = expr.position(); Ok(Stmt::ReturnWithVal(Box::new(( @@ -2370,17 +2290,17 @@ fn parse_stmt( } } - Token::Let => parse_let(input, state, Normal, level + 1, if_expr, stmt_expr), - Token::Const => parse_let(input, state, Constant, level + 1, if_expr, stmt_expr), - Token::Import => parse_import(input, state, level + 1, if_expr, stmt_expr), + Token::Let => parse_let(input, state, Normal, settings.level_up()), + Token::Const => parse_let(input, state, Constant, settings.level_up()), + Token::Import => parse_import(input, state, settings.level_up()), #[cfg(not(feature = "no_module"))] - Token::Export if !is_global => Err(PERR::WrongExport.into_err(*pos)), + Token::Export if !settings.is_global => Err(PERR::WrongExport.into_err(settings.pos)), #[cfg(not(feature = "no_module"))] - Token::Export => parse_export(input, state, level + 1), + Token::Export => parse_export(input, state, settings.level_up()), - _ => parse_expr_stmt(input, state, level + 1, if_expr, stmt_expr), + _ => parse_expr_stmt(input, state, settings.level_up()), } } @@ -2390,15 +2310,10 @@ fn parse_fn( input: &mut TokenStream, state: &mut ParseState, access: FnAccess, - level: usize, - if_expr: bool, - stmt_expr: bool, + mut settings: ParseSettings, ) -> Result { - let pos = eat_token(input, Token::Fn); - - if level > state.max_expr_depth { - return Err(PERR::ExprTooDeep.into_err(pos)); - } + settings.pos = eat_token(input, Token::Fn); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; let name = match input.next().unwrap() { (Token::Identifier(s), _) => s, @@ -2463,7 +2378,10 @@ fn parse_fn( // Parse function body let body = match input.peek().unwrap() { - (Token::LeftBrace, _) => parse_block(input, state, false, level + 1, if_expr, stmt_expr)?, + (Token::LeftBrace, _) => { + settings.is_breakable = false; + parse_block(input, state, settings.level_up())? + } (_, pos) => return Err(PERR::FnMissingBody(name).into_err(*pos)), }; @@ -2474,7 +2392,7 @@ fn parse_fn( access, params, body, - pos, + pos: settings.pos, }) } @@ -2500,9 +2418,17 @@ fn parse_global_level( match input.peek().unwrap() { #[cfg(not(feature = "no_function"))] - (Token::Fn, _) => { + (Token::Fn, pos) => { let mut state = ParseState::new(max_function_expr_depth); - let func = parse_fn(input, &mut state, access, 0, true, true)?; + let settings = ParseSettings { + allow_if_expr: true, + allow_stmt_expr: true, + is_global: false, + is_breakable: false, + level: 0, + pos: *pos, + }; + let func = parse_fn(input, &mut state, access, settings)?; // Qualifiers (none) + function name + number of arguments. let hash = calc_fn_hash(empty(), &func.name, func.params.len(), empty()); @@ -2520,8 +2446,17 @@ fn parse_global_level( _ => (), } } + // Actual statement - let stmt = parse_stmt(input, &mut state, false, true, 0, true, true)?; + let settings = ParseSettings { + allow_if_expr: true, + allow_stmt_expr: true, + is_global: true, + is_breakable: false, + level: 0, + pos: Position::none(), + }; + let stmt = parse_stmt(input, &mut state, settings)?; let need_semicolon = !stmt.is_self_terminated(); @@ -2565,7 +2500,15 @@ impl Engine { optimization_level: OptimizationLevel, ) -> Result { let mut state = ParseState::new(self.max_expr_depth); - let expr = parse_expr(input, &mut state, 0, false, false)?; + let settings = ParseSettings { + allow_if_expr: false, + allow_stmt_expr: false, + is_global: true, + is_breakable: false, + level: 0, + pos: Position::none(), + }; + let expr = parse_expr(input, &mut state, settings)?; match input.peek().unwrap() { (Token::EOF, _) => (), From e4cca7620f878f626e2744c1420624634d35e1b5 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 11 Jun 2020 22:18:30 +0800 Subject: [PATCH 26/53] Better debug formatting for StaticVec. --- src/any.rs | 30 +++++++++++++++--------------- src/fn_native.rs | 2 +- src/module.rs | 2 +- src/utils.rs | 4 +--- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/any.rs b/src/any.rs index 0647d219..a50d6820 100644 --- a/src/any.rs +++ b/src/any.rs @@ -202,17 +202,17 @@ impl fmt::Display for Dynamic { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { Union::Unit(_) => write!(f, ""), - Union::Bool(value) => write!(f, "{}", value), - Union::Str(value) => write!(f, "{}", value), - Union::Char(value) => write!(f, "{}", value), - Union::Int(value) => write!(f, "{}", value), + Union::Bool(value) => fmt::Display::fmt(value, f), + Union::Str(value) => fmt::Display::fmt(value, f), + Union::Char(value) => fmt::Display::fmt(value, f), + Union::Int(value) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_float"))] - Union::Float(value) => write!(f, "{}", value), + Union::Float(value) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_index"))] - Union::Array(value) => write!(f, "{:?}", value), + Union::Array(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_object"))] Union::Map(value) => write!(f, "#{:?}", value), - Union::Module(value) => write!(f, "{:?}", value), + Union::Module(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_std"))] Union::Variant(value) if value.is::() => write!(f, ""), @@ -224,18 +224,18 @@ impl fmt::Display for Dynamic { impl fmt::Debug for Dynamic { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { - Union::Unit(value) => write!(f, "{:?}", value), - Union::Bool(value) => write!(f, "{:?}", value), - Union::Str(value) => write!(f, "{:?}", value), - Union::Char(value) => write!(f, "{:?}", value), - Union::Int(value) => write!(f, "{:?}", value), + Union::Unit(value) => fmt::Debug::fmt(value, f), + Union::Bool(value) => fmt::Debug::fmt(value, f), + Union::Str(value) => fmt::Debug::fmt(value, f), + Union::Char(value) => fmt::Debug::fmt(value, f), + Union::Int(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_float"))] - Union::Float(value) => write!(f, "{:?}", value), + Union::Float(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_index"))] - Union::Array(value) => write!(f, "{:?}", value), + Union::Array(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_object"))] Union::Map(value) => write!(f, "#{:?}", value), - Union::Module(value) => write!(f, "{:?}", value), + Union::Module(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_std"))] Union::Variant(value) if value.is::() => write!(f, ""), diff --git a/src/fn_native.rs b/src/fn_native.rs index bf6219e1..fc478512 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -82,7 +82,7 @@ impl fmt::Debug for CallableFunction { Self::Pure(_) => write!(f, "NativePureFunction"), Self::Method(_) => write!(f, "NativeMethod"), Self::Iterator(_) => write!(f, "NativeIterator"), - Self::Script(fn_def) => write!(f, "{:?}", fn_def), + Self::Script(fn_def) => fmt::Debug::fmt(fn_def, f), } } } diff --git a/src/module.rs b/src/module.rs index e607a3e4..91fa3388 100644 --- a/src/module.rs +++ b/src/module.rs @@ -65,7 +65,7 @@ impl fmt::Debug for Module { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "", + "", self.variables, self.functions.len(), ) diff --git a/src/utils.rs b/src/utils.rs index 4a8cd3c8..bedf73b2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -542,9 +542,7 @@ impl StaticVec { impl fmt::Debug for StaticVec { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "[ ")?; - self.iter().try_for_each(|v| write!(f, "{:?}, ", v))?; - write!(f, "]") + fmt::Debug::fmt(&self.iter().collect::>(), f) } } From d92613eaf6700145539f18f9bf7f8e980ea42f41 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 11 Jun 2020 23:08:00 +0800 Subject: [PATCH 27/53] Fix parse positions. --- src/parser.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 0cd50b57..f6ed1ccc 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -16,7 +16,7 @@ use crate::stdlib::{ char, collections::HashMap, format, - iter::{empty, Peekable}, + iter::empty, mem, num::NonZeroUsize, ops::{Add, Deref, DerefMut}, @@ -751,15 +751,13 @@ fn parse_paren_expr( // ( xxx ) (Token::RightParen, _) => Ok(expr), // ( - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(settings.pos)) - } + (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), // ( xxx ??? (_, pos) => Err(PERR::MissingToken( Token::RightParen.into(), "for a matching ( in this expression".into(), ) - .into_err(settings.pos)), + .into_err(pos)), } } @@ -769,10 +767,9 @@ fn parse_call_expr( state: &mut ParseState, id: String, mut modules: Option>, - mut settings: ParseSettings, + settings: ParseSettings, ) -> Result { - let (token, token_pos) = input.peek().unwrap(); - settings.pos = *token_pos; + let (token, _) = input.peek().unwrap(); settings.ensure_level_within_max_limit(state.max_expr_depth)?; let mut args = StaticVec::new(); @@ -888,7 +885,7 @@ fn parse_index_chain( input: &mut TokenStream, state: &mut ParseState, lhs: Expr, - settings: ParseSettings, + mut settings: ParseSettings, ) -> Result { settings.ensure_level_within_max_limit(state.max_expr_depth)?; @@ -1031,11 +1028,12 @@ fn parse_index_chain( match input.peek().unwrap() { // If another indexing level, right-bind it (Token::LeftBracket, _) => { - let idx_pos = eat_token(input, Token::LeftBracket); + let prev_pos = settings.pos; + settings.pos = eat_token(input, Token::LeftBracket); // Recursively parse the indexing chain, right-binding each let idx_expr = parse_index_chain(input, state, idx_expr, settings.level_up())?; // Indexing binds to right - Ok(Expr::Index(Box::new((lhs, idx_expr, settings.pos)))) + Ok(Expr::Index(Box::new((lhs, idx_expr, prev_pos)))) } // Otherwise terminate the indexing chain _ => { @@ -1257,11 +1255,13 @@ fn parse_primary( } let (token, token_pos) = input.next().unwrap(); + settings.pos = token_pos; root_expr = match (root_expr, token) { // Function call (Expr::Variable(x), Token::LeftParen) => { let ((name, pos), modules, _, _) = *x; + settings.pos = pos; parse_call_expr(input, state, name, modules, settings.level_up())? } (Expr::Property(_), _) => unreachable!(), @@ -1269,6 +1269,7 @@ fn parse_primary( (Expr::Variable(x), Token::DoubleColon) => match input.next().unwrap() { (Token::Identifier(id2), pos2) => { let ((name, pos), mut modules, _, index) = *x; + if let Some(ref mut modules) = modules { modules.push((name, pos)); } else { @@ -1284,7 +1285,6 @@ fn parse_primary( // Indexing #[cfg(not(feature = "no_index"))] (expr, Token::LeftBracket) => { - settings.pos = token_pos; parse_index_chain(input, state, expr, settings.level_up())? } // Unknown postfix operator From 6cc27eb9f481c765fa72786e6faa10374a914cbe Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 11 Jun 2020 23:21:39 +0800 Subject: [PATCH 28/53] Better error messages. --- src/parser.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/parser.rs b/src/parser.rs index f6ed1ccc..2eed69b1 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1416,9 +1416,11 @@ fn make_assignment_stmt<'a>( pos: Position, ) -> Result { match &lhs { + // var (non-indexed) = rhs Expr::Variable(x) if x.3.is_none() => { Ok(Expr::Assignment(Box::new((lhs, fn_name.into(), rhs, pos)))) } + // var (indexed) = rhs Expr::Variable(x) => { let ((name, name_pos), _, _, index) = x.as_ref(); match state.stack[(state.len() - index.unwrap().get())].1 { @@ -1432,10 +1434,13 @@ fn make_assignment_stmt<'a>( ScopeEntryType::Module => unreachable!(), } } + // xxx[???] = rhs, xxx.??? = rhs Expr::Index(x) | Expr::Dot(x) => match &x.0 { + // var[???] (non-indexed) = rhs, var.??? (non-indexed) = rhs Expr::Variable(x) if x.3.is_none() => { Ok(Expr::Assignment(Box::new((lhs, fn_name.into(), rhs, pos)))) } + // var[???] (indexed) = rhs, var.??? (indexed) = rhs Expr::Variable(x) => { let ((name, name_pos), _, _, index) = x.as_ref(); match state.stack[(state.len() - index.unwrap().get())].1 { @@ -1449,11 +1454,18 @@ fn make_assignment_stmt<'a>( ScopeEntryType::Module => unreachable!(), } } + // expr[???] = rhs, expr.??? = rhs _ => Err(PERR::AssignmentToCopy.into_err(x.0.position())), }, + // const_expr = rhs expr if expr.is_constant() => { Err(PERR::AssignmentToConstant("".into()).into_err(lhs.position())) } + // ??? && ??? = rhs, ??? || ??? = rhs + Expr::And(_) | Expr::Or(_) => { + Err(PERR::BadInput("Possibly a typo of '=='?".to_string()).into_err(pos)) + } + // expr = rhs _ => Err(PERR::AssignmentToCopy.into_err(lhs.position())), } } From 9d913495133d1efe0fe3e36e5b6fbc947d3915b6 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 12 Jun 2020 18:04:16 +0800 Subject: [PATCH 29/53] Refine position display in error messages. --- RELEASES.md | 1 + examples/repl.rs | 48 +++++----- examples/rhai_runner.rs | 37 ++++---- src/engine.rs | 4 +- src/error.rs | 191 ++++++++++++++++++++-------------------- src/result.rs | 148 +++++++++++++++---------------- tests/call_fn.rs | 16 ++-- tests/constants.rs | 8 +- tests/expressions.rs | 2 + tests/looping.rs | 12 ++- tests/modules.rs | 21 ++++- tests/stack.rs | 6 +- 12 files changed, 256 insertions(+), 238 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 011c5c80..0b3136c1 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -13,6 +13,7 @@ Breaking changes * Callback closure passed to `Engine::on_progress` now takes `&u64` instead of `u64` to be consistent with other callback signatures. * `Engine::register_indexer` is renamed to `Engine::register_indexer_get`. * `Module::set_indexer_fn` is renamed to `Module::set_indexer_get_fn`. +* The tuple `ParseError` now exposes the internal fields and the `ParseError::error_type` and `ParseError::position` methods are removed. The first tuple field is the `ParseErrorType` and the second tuple field is the `Position`. New features ------------ diff --git a/examples/repl.rs b/examples/repl.rs index f980a98e..4a3bd561 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -7,43 +7,41 @@ use std::io::{stdin, stdout, Write}; fn print_error(input: &str, err: EvalAltResult) { let lines: Vec<_> = input.trim().split('\n').collect(); + let pos = err.position(); let line_no = if lines.len() > 1 { - match err.position() { - p if p.is_none() => "".to_string(), - p => format!("{}: ", p.line().unwrap()), + if pos.is_none() { + "".to_string() + } else { + format!("{}: ", pos.line().unwrap()) } } else { "".to_string() }; // Print error - let pos = err.position(); let pos_text = format!(" ({})", pos); - match pos { - p if p.is_none() => { - // No position - println!("{}", err); - } - p => { - // Specific position - println!("{}{}", line_no, lines[p.line().unwrap() - 1]); + if pos.is_none() { + // No position + println!("{}", err); + } else { + // Specific position + println!("{}{}", line_no, lines[pos.line().unwrap() - 1]); - let err_text = match err { - EvalAltResult::ErrorRuntime(err, _) if !err.is_empty() => { - format!("Runtime error: {}", err) - } - err => err.to_string(), - }; + let err_text = match err { + EvalAltResult::ErrorRuntime(err, _) if !err.is_empty() => { + format!("Runtime error: {}", err) + } + err => err.to_string(), + }; - println!( - "{0:>1$} {2}", - "^", - line_no.len() + p.position().unwrap(), - err_text.replace(&pos_text, "") - ); - } + println!( + "{0:>1$} {2}", + "^", + line_no.len() + pos.position().unwrap(), + err_text.replace(&pos_text, "") + ); } } diff --git a/examples/rhai_runner.rs b/examples/rhai_runner.rs index c5c5128d..135269ce 100644 --- a/examples/rhai_runner.rs +++ b/examples/rhai_runner.rs @@ -1,4 +1,4 @@ -use rhai::{Engine, EvalAltResult}; +use rhai::{Engine, EvalAltResult, Position}; #[cfg(not(feature = "no_optimize"))] use rhai::OptimizationLevel; @@ -6,15 +6,17 @@ use rhai::OptimizationLevel; use std::{env, fs::File, io::Read, process::exit}; fn eprint_error(input: &str, err: EvalAltResult) { - fn eprint_line(lines: &[&str], line: usize, pos: usize, err: &str) { + fn eprint_line(lines: &[&str], pos: Position, err: &str) { + let line = pos.line().unwrap(); + let line_no = format!("{}: ", line); - let pos_text = format!(" (line {}, position {})", line, pos); + let pos_text = format!(" ({})", pos); eprintln!("{}{}", line_no, lines[line - 1]); eprintln!( "{:>1$} {2}", "^", - line_no.len() + pos, + line_no.len() + pos.position().unwrap(), err.replace(&pos_text, "") ); eprintln!(""); @@ -25,22 +27,19 @@ fn eprint_error(input: &str, err: EvalAltResult) { // Print error let pos = err.position(); - match pos { - p if p.is_none() => { - // No position - eprintln!("{}", err); - } - p => { - // Specific position - let err_text = match err { - EvalAltResult::ErrorRuntime(err, _) if !err.is_empty() => { - format!("Runtime error: {}", err) - } - err => err.to_string(), - }; + if pos.is_none() { + // No position + eprintln!("{}", err); + } else { + // Specific position + let err_text = match err { + EvalAltResult::ErrorRuntime(err, _) if !err.is_empty() => { + format!("Runtime error: {}", err) + } + err => err.to_string(), + }; - eprint_line(&lines, p.line().unwrap(), p.position().unwrap(), &err_text) - } + eprint_line(&lines, pos, &err_text) } } diff --git a/src/engine.rs b/src/engine.rs index 8c860b7f..b3d0536d 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -871,9 +871,7 @@ impl Engine { // If new functions are defined within the eval string, it is an error if ast.lib().num_fn() != 0 { - return Err(Box::new(EvalAltResult::ErrorParsing( - ParseErrorType::WrongFnDefinition.into_err(Position::none()), - ))); + return Err(ParseErrorType::WrongFnDefinition.into()); } let statements = mem::take(ast.statements_mut()); diff --git a/src/error.rs b/src/error.rs index 7bc60a1c..d75af495 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,6 @@ //! Module containing error definitions for the parsing process. +use crate::result::EvalAltResult; use crate::token::Position; use crate::stdlib::{boxed::Box, char, error::Error, fmt, string::String}; @@ -123,113 +124,111 @@ impl ParseErrorType { pub(crate) fn into_err(self, pos: Position) -> ParseError { ParseError(Box::new(self), pos) } + + pub(crate) fn desc(&self) -> &str { + match self { + Self::BadInput(p) => p, + Self::UnexpectedEOF => "Script is incomplete", + Self::UnknownOperator(_) => "Unknown operator", + Self::MissingToken(_, _) => "Expecting a certain token that is missing", + Self::MalformedCallExpr(_) => "Invalid expression in function call arguments", + Self::MalformedIndexExpr(_) => "Invalid index in indexing expression", + Self::MalformedInExpr(_) => "Invalid 'in' expression", + Self::DuplicatedProperty(_) => "Duplicated property in object map literal", + Self::ForbiddenConstantExpr(_) => "Expecting a constant", + Self::PropertyExpected => "Expecting name of a property", + Self::VariableExpected => "Expecting name of a variable", + Self::ExprExpected(_) => "Expecting an expression", + Self::FnMissingName => "Expecting name in function declaration", + Self::FnMissingParams(_) => "Expecting parameters in function declaration", + Self::FnDuplicatedParam(_,_) => "Duplicated parameters in function declaration", + Self::FnMissingBody(_) => "Expecting body statement block for function declaration", + Self::WrongFnDefinition => "Function definitions must be at global level and cannot be inside a block or another function", + Self::DuplicatedExport(_) => "Duplicated variable/function in export statement", + Self::WrongExport => "Export statement can only appear at global level", + Self::AssignmentToCopy => "Only a copy of the value is change with this assignment", + Self::AssignmentToConstant(_) => "Cannot assign to a constant value", + Self::ExprTooDeep => "Expression exceeds maximum complexity", + Self::LoopBreak => "Break statement should only be used inside a loop" + } + } +} + +impl fmt::Display for ParseErrorType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BadInput(s) | ParseErrorType::MalformedCallExpr(s) => { + write!(f, "{}", if s.is_empty() { self.desc() } else { s }) + } + Self::ForbiddenConstantExpr(s) => { + write!(f, "Expecting a constant to assign to '{}'", s) + } + Self::UnknownOperator(s) => write!(f, "{}: '{}'", self.desc(), s), + + Self::MalformedIndexExpr(s) => { + write!(f, "{}", if s.is_empty() { self.desc() } else { s }) + } + + Self::MalformedInExpr(s) => write!(f, "{}", if s.is_empty() { self.desc() } else { s }), + + Self::DuplicatedProperty(s) => { + write!(f, "Duplicated property '{}' for object map literal", s) + } + + Self::ExprExpected(s) => write!(f, "Expecting {} expression", s), + + Self::FnMissingParams(s) => write!(f, "Expecting parameters for function '{}'", s), + + Self::FnMissingBody(s) => { + write!(f, "Expecting body statement block for function '{}'", s) + } + + Self::FnDuplicatedParam(s, arg) => { + write!(f, "Duplicated parameter '{}' for function '{}'", arg, s) + } + + Self::DuplicatedExport(s) => write!( + f, + "Duplicated variable/function '{}' in export statement", + s + ), + + Self::MissingToken(token, s) => write!(f, "Expecting '{}' {}", token, s), + + Self::AssignmentToConstant(s) if s.is_empty() => write!(f, "{}", self.desc()), + Self::AssignmentToConstant(s) => write!(f, "Cannot assign to constant '{}'", s), + _ => write!(f, "{}", self.desc()), + } + } } /// Error when parsing a script. #[derive(Debug, Eq, PartialEq, Clone, Hash)] -pub struct ParseError(pub(crate) Box, pub(crate) Position); - -impl ParseError { - /// Get the parse error. - pub fn error_type(&self) -> &ParseErrorType { - &self.0 - } - - /// Get the location in the script of the error. - pub fn position(&self) -> Position { - self.1 - } - - pub(crate) fn desc(&self) -> &str { - match self.0.as_ref() { - ParseErrorType::BadInput(p) => p, - ParseErrorType::UnexpectedEOF => "Script is incomplete", - ParseErrorType::UnknownOperator(_) => "Unknown operator", - ParseErrorType::MissingToken(_, _) => "Expecting a certain token that is missing", - ParseErrorType::MalformedCallExpr(_) => "Invalid expression in function call arguments", - ParseErrorType::MalformedIndexExpr(_) => "Invalid index in indexing expression", - ParseErrorType::MalformedInExpr(_) => "Invalid 'in' expression", - ParseErrorType::DuplicatedProperty(_) => "Duplicated property in object map literal", - ParseErrorType::ForbiddenConstantExpr(_) => "Expecting a constant", - ParseErrorType::PropertyExpected => "Expecting name of a property", - ParseErrorType::VariableExpected => "Expecting name of a variable", - ParseErrorType::ExprExpected(_) => "Expecting an expression", - ParseErrorType::FnMissingName => "Expecting name in function declaration", - ParseErrorType::FnMissingParams(_) => "Expecting parameters in function declaration", - ParseErrorType::FnDuplicatedParam(_,_) => "Duplicated parameters in function declaration", - ParseErrorType::FnMissingBody(_) => "Expecting body statement block for function declaration", - ParseErrorType::WrongFnDefinition => "Function definitions must be at global level and cannot be inside a block or another function", - ParseErrorType::DuplicatedExport(_) => "Duplicated variable/function in export statement", - ParseErrorType::WrongExport => "Export statement can only appear at global level", - ParseErrorType::AssignmentToCopy => "Only a copy of the value is change with this assignment", - ParseErrorType::AssignmentToConstant(_) => "Cannot assign to a constant value", - ParseErrorType::ExprTooDeep => "Expression exceeds maximum complexity", - ParseErrorType::LoopBreak => "Break statement should only be used inside a loop" - } - } -} +pub struct ParseError(pub Box, pub Position); impl Error for ParseError {} impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.0.as_ref() { - ParseErrorType::BadInput(s) | ParseErrorType::MalformedCallExpr(s) => { - write!(f, "{}", if s.is_empty() { self.desc() } else { s })? - } - ParseErrorType::ForbiddenConstantExpr(s) => { - write!(f, "Expecting a constant to assign to '{}'", s)? - } - ParseErrorType::UnknownOperator(s) => write!(f, "{}: '{}'", self.desc(), s)?, - - ParseErrorType::MalformedIndexExpr(s) => { - write!(f, "{}", if s.is_empty() { self.desc() } else { s })? - } - - ParseErrorType::MalformedInExpr(s) => { - write!(f, "{}", if s.is_empty() { self.desc() } else { s })? - } - - ParseErrorType::DuplicatedProperty(s) => { - write!(f, "Duplicated property '{}' for object map literal", s)? - } - - ParseErrorType::ExprExpected(s) => write!(f, "Expecting {} expression", s)?, - - ParseErrorType::FnMissingParams(s) => { - write!(f, "Expecting parameters for function '{}'", s)? - } - - ParseErrorType::FnMissingBody(s) => { - write!(f, "Expecting body statement block for function '{}'", s)? - } - - ParseErrorType::FnDuplicatedParam(s, arg) => { - write!(f, "Duplicated parameter '{}' for function '{}'", arg, s)? - } - - ParseErrorType::DuplicatedExport(s) => write!( - f, - "Duplicated variable/function '{}' in export statement", - s - )?, - - ParseErrorType::MissingToken(token, s) => write!(f, "Expecting '{}' {}", token, s)?, - - ParseErrorType::AssignmentToConstant(s) if s.is_empty() => { - write!(f, "{}", self.desc())? - } - ParseErrorType::AssignmentToConstant(s) => { - write!(f, "Cannot assign to constant '{}'", s)? - } - _ => write!(f, "{}", self.desc())?, - } + fmt::Display::fmt(&self.0, f)?; + // Do not write any position if None if !self.1.is_none() { - // Do not write any position if None - Ok(()) - } else { - write!(f, " ({})", self.1) + write!(f, " ({})", self.1)?; } + + Ok(()) + } +} + +impl From for Box { + fn from(err: ParseErrorType) -> Self { + Box::new(EvalAltResult::ErrorParsing(err, Position::none())) + } +} + +impl From for Box { + fn from(err: ParseError) -> Self { + Box::new(EvalAltResult::ErrorParsing(*err.0, err.1)) } } diff --git a/src/result.rs b/src/result.rs index 77102085..36bc18f6 100644 --- a/src/result.rs +++ b/src/result.rs @@ -1,7 +1,7 @@ //! Module containing error definitions for the evaluation process. use crate::any::Dynamic; -use crate::error::ParseError; +use crate::error::ParseErrorType; use crate::parser::INT; use crate::token::Position; @@ -23,7 +23,7 @@ use crate::stdlib::path::PathBuf; #[derive(Debug)] pub enum EvalAltResult { /// Syntax error. - ErrorParsing(ParseError), + ErrorParsing(ParseErrorType, Position), /// Error reading from a script file. Wrapped value is the path of the script file. /// @@ -101,7 +101,7 @@ impl EvalAltResult { #[cfg(not(feature = "no_std"))] Self::ErrorReadingScriptFile(_, _, _) => "Cannot read from script file", - Self::ErrorParsing(p) => p.desc(), + Self::ErrorParsing(p, _) => p.desc(), Self::ErrorInFunctionCall(_, _, _) => "Error in called function", Self::ErrorFunctionNotFound(_, _) => "Function not found", Self::ErrorBooleanArgMismatch(_, _) => "Boolean operator expects boolean operands", @@ -153,95 +153,89 @@ impl Error for EvalAltResult {} impl fmt::Display for EvalAltResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let desc = self.desc(); + let pos = self.position(); match self { #[cfg(not(feature = "no_std"))] - Self::ErrorReadingScriptFile(path, pos, err) if pos.is_none() => { - write!(f, "{} '{}': {}", desc, path.display(), err) - } - #[cfg(not(feature = "no_std"))] - Self::ErrorReadingScriptFile(path, pos, err) => { - write!(f, "{} '{}': {} ({})", desc, path.display(), err, pos) + Self::ErrorReadingScriptFile(path, _, err) => { + write!(f, "{} '{}': {}", desc, path.display(), err)? } - Self::ErrorParsing(p) => write!(f, "Syntax error: {}", p), + Self::ErrorParsing(p, _) => write!(f, "Syntax error: {}", p)?, - Self::ErrorInFunctionCall(s, err, pos) => { - write!(f, "Error in call to function '{}' ({}): {}", s, pos, err) + Self::ErrorInFunctionCall(s, err, _) => { + write!(f, "Error in call to function '{}' : {}", s, err)? } - Self::ErrorFunctionNotFound(s, pos) - | Self::ErrorVariableNotFound(s, pos) - | Self::ErrorModuleNotFound(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos), + Self::ErrorFunctionNotFound(s, _) + | Self::ErrorVariableNotFound(s, _) + | Self::ErrorModuleNotFound(s, _) => write!(f, "{}: '{}'", desc, s)?, - Self::ErrorDotExpr(s, pos) if !s.is_empty() => write!(f, "{} {} ({})", desc, s, pos), + Self::ErrorDotExpr(s, _) if !s.is_empty() => write!(f, "{} {}", desc, s)?, - Self::ErrorIndexingType(_, pos) - | Self::ErrorNumericIndexExpr(pos) - | Self::ErrorStringIndexExpr(pos) - | Self::ErrorImportExpr(pos) - | Self::ErrorLogicGuard(pos) - | Self::ErrorFor(pos) - | Self::ErrorAssignmentToUnknownLHS(pos) - | Self::ErrorInExpr(pos) - | Self::ErrorDotExpr(_, pos) - | Self::ErrorTooManyOperations(pos) - | Self::ErrorTooManyModules(pos) - | Self::ErrorStackOverflow(pos) - | Self::ErrorTerminated(pos) => write!(f, "{} ({})", desc, pos), + Self::ErrorIndexingType(_, _) + | Self::ErrorNumericIndexExpr(_) + | Self::ErrorStringIndexExpr(_) + | Self::ErrorImportExpr(_) + | Self::ErrorLogicGuard(_) + | Self::ErrorFor(_) + | Self::ErrorAssignmentToUnknownLHS(_) + | Self::ErrorInExpr(_) + | Self::ErrorDotExpr(_, _) + | Self::ErrorTooManyOperations(_) + | Self::ErrorTooManyModules(_) + | Self::ErrorStackOverflow(_) + | Self::ErrorTerminated(_) => write!(f, "{}", desc)?, - Self::ErrorRuntime(s, pos) => { - write!(f, "{} ({})", if s.is_empty() { desc } else { s }, pos) + Self::ErrorRuntime(s, _) => write!(f, "{}", if s.is_empty() { desc } else { s })?, + + Self::ErrorAssignmentToConstant(s, _) => write!(f, "{}: '{}'", desc, s)?, + Self::ErrorMismatchOutputType(s, _) => write!(f, "{}: {}", desc, s)?, + Self::ErrorArithmetic(s, _) => write!(f, "{}", s)?, + + Self::ErrorLoopBreak(_, _) => write!(f, "{}", desc)?, + Self::Return(_, _) => write!(f, "{}", desc)?, + + Self::ErrorBooleanArgMismatch(op, _) => { + write!(f, "{} operator expects boolean operands", op)? } - - Self::ErrorAssignmentToConstant(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos), - Self::ErrorMismatchOutputType(s, pos) => write!(f, "{}: {} ({})", desc, s, pos), - Self::ErrorArithmetic(s, pos) => write!(f, "{} ({})", s, pos), - - Self::ErrorLoopBreak(_, pos) => write!(f, "{} ({})", desc, pos), - Self::Return(_, pos) => write!(f, "{} ({})", desc, pos), - - Self::ErrorBooleanArgMismatch(op, pos) => { - write!(f, "{} operator expects boolean operands ({})", op, pos) + Self::ErrorCharMismatch(_) => write!(f, "string indexing expects a character value")?, + Self::ErrorArrayBounds(_, index, _) if *index < 0 => { + write!(f, "{}: {} < 0", desc, index)? } - 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) - } - Self::ErrorArrayBounds(0, _, pos) => write!(f, "{} ({})", desc, pos), - Self::ErrorArrayBounds(1, index, pos) => write!( + Self::ErrorArrayBounds(0, _, _) => write!(f, "{}", desc)?, + Self::ErrorArrayBounds(1, index, _) => write!( f, - "Array index {} is out of bounds: only one element in the array ({})", - index, pos - ), - Self::ErrorArrayBounds(max, index, pos) => write!( + "Array index {} is out of bounds: only one element in the array", + index + )?, + Self::ErrorArrayBounds(max, index, _) => write!( f, - "Array index {} is out of bounds: only {} elements in the array ({})", - index, max, pos - ), - Self::ErrorStringBounds(_, index, pos) if *index < 0 => { - write!(f, "{}: {} < 0 ({})", desc, index, pos) + "Array index {} is out of bounds: only {} elements in the array", + index, max + )?, + Self::ErrorStringBounds(_, index, _) if *index < 0 => { + write!(f, "{}: {} < 0", desc, index)? } - Self::ErrorStringBounds(0, _, pos) => write!(f, "{} ({})", desc, pos), - Self::ErrorStringBounds(1, index, pos) => write!( + Self::ErrorStringBounds(0, _, _) => write!(f, "{}", desc)?, + Self::ErrorStringBounds(1, index, _) => write!( f, - "String index {} is out of bounds: only one character in the string ({})", - index, pos - ), - Self::ErrorStringBounds(max, index, pos) => write!( + "String index {} is out of bounds: only one character in the string", + index + )?, + Self::ErrorStringBounds(max, index, _) => write!( f, - "String index {} is out of bounds: only {} characters in the string ({})", - index, max, pos - ), + "String index {} is out of bounds: only {} characters in the string", + index, max + )?, } - } -} -impl From for Box { - fn from(err: ParseError) -> Self { - Box::new(EvalAltResult::ErrorParsing(err)) + // Do not write any position if None + if !pos.is_none() { + write!(f, " ({})", pos)?; + } + + Ok(()) } } @@ -261,9 +255,8 @@ impl EvalAltResult { #[cfg(not(feature = "no_std"))] Self::ErrorReadingScriptFile(_, pos, _) => *pos, - Self::ErrorParsing(err) => err.position(), - - Self::ErrorFunctionNotFound(_, pos) + Self::ErrorParsing(_, pos) + | Self::ErrorFunctionNotFound(_, pos) | Self::ErrorInFunctionCall(_, _, pos) | Self::ErrorBooleanArgMismatch(_, pos) | Self::ErrorCharMismatch(pos) @@ -299,9 +292,8 @@ impl EvalAltResult { #[cfg(not(feature = "no_std"))] Self::ErrorReadingScriptFile(_, pos, _) => *pos = new_position, - Self::ErrorParsing(err) => err.1 = new_position, - - Self::ErrorFunctionNotFound(_, pos) + Self::ErrorParsing(_, pos) + | Self::ErrorFunctionNotFound(_, pos) | Self::ErrorInFunctionCall(_, _, pos) | Self::ErrorBooleanArgMismatch(_, pos) | Self::ErrorCharMismatch(pos) diff --git a/tests/call_fn.rs b/tests/call_fn.rs index e3c61765..0bed8d7b 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -1,19 +1,17 @@ #![cfg(not(feature = "no_function"))] -use rhai::{Engine, EvalAltResult, Func, ParseErrorType, Scope, INT}; +use rhai::{Engine, EvalAltResult, Func, ParseError, ParseErrorType, Scope, INT}; #[test] fn test_fn() -> Result<(), Box> { let engine = Engine::new(); // Expect duplicated parameters error - match engine - .compile("fn hello(x, x) { x }") - .expect_err("should be error") - .error_type() - { - ParseErrorType::FnDuplicatedParam(f, p) if f == "hello" && p == "x" => (), - _ => assert!(false, "wrong error"), - } + assert!(matches!( + engine + .compile("fn hello(x, x) { x }") + .expect_err("should be error"), + ParseError(x, _) if *x == ParseErrorType::FnDuplicatedParam("hello".to_string(), "x".to_string()) + )); Ok(()) } diff --git a/tests/constants.rs b/tests/constants.rs index d5e0820a..266b6ac2 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -7,14 +7,16 @@ fn test_constant() -> Result<(), Box> { assert_eq!(engine.eval::("const x = 123; x")?, 123); assert!(matches!( - *engine.eval::("const x = 123; x = 42;").expect_err("expects error"), - EvalAltResult::ErrorParsing(err) if err.error_type() == &ParseErrorType::AssignmentToConstant("x".to_string()) + *engine + .eval::("const x = 123; x = 42;") + .expect_err("expects error"), + EvalAltResult::ErrorParsing(ParseErrorType::AssignmentToConstant(x), _) if x == "x" )); #[cfg(not(feature = "no_index"))] assert!(matches!( *engine.eval::("const x = [1, 2, 3, 4, 5]; x[2] = 42;").expect_err("expects error"), - EvalAltResult::ErrorParsing(err) if err.error_type() == &ParseErrorType::AssignmentToConstant("x".to_string()) + EvalAltResult::ErrorParsing(ParseErrorType::AssignmentToConstant(x), _) if x == "x" )); Ok(()) diff --git a/tests/expressions.rs b/tests/expressions.rs index 75790f00..771d1d02 100644 --- a/tests/expressions.rs +++ b/tests/expressions.rs @@ -21,6 +21,8 @@ fn test_expressions() -> Result<(), Box> { assert!(engine.eval_expression::<()>("x = 42").is_err()); assert!(engine.compile_expression("let x = 42").is_err()); + engine.compile("40 + { let x = 2; x }")?; + Ok(()) } diff --git a/tests/looping.rs b/tests/looping.rs index a973c98f..3a4804ce 100644 --- a/tests/looping.rs +++ b/tests/looping.rs @@ -1,4 +1,4 @@ -use rhai::{Engine, EvalAltResult, INT}; +use rhai::{Engine, EvalAltResult, ParseError, ParseErrorType, INT}; #[test] fn test_loop() -> Result<(), Box> { @@ -26,5 +26,15 @@ fn test_loop() -> Result<(), Box> { 21 ); + assert!(matches!( + engine.compile("let x = 0; break;").expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::LoopBreak + )); + + assert!(matches!( + engine.compile("let x = 0; if x > 0 { continue; }").expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::LoopBreak + )); + Ok(()) } diff --git a/tests/modules.rs b/tests/modules.rs index a063e1bd..10c138ad 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -1,5 +1,7 @@ #![cfg(not(feature = "no_module"))] -use rhai::{module_resolvers, Engine, EvalAltResult, Module, Scope, INT}; +use rhai::{ + module_resolvers, Engine, EvalAltResult, Module, ParseError, ParseErrorType, Scope, INT, +}; #[test] fn test_module() { @@ -231,3 +233,20 @@ fn test_module_from_ast() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_module_export() -> Result<(), Box> { + let engine = Engine::new(); + + assert!(matches!( + engine.compile(r"let x = 10; { export x; }").expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::WrongExport + )); + + assert!(matches!( + engine.compile(r"fn abc(x) { export x; }").expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::WrongExport + )); + + Ok(()) +} diff --git a/tests/stack.rs b/tests/stack.rs index 1ba72e6a..29828fef 100644 --- a/tests/stack.rs +++ b/tests/stack.rs @@ -1,5 +1,5 @@ #![cfg(not(feature = "unchecked"))] -use rhai::{Engine, EvalAltResult, ParseErrorType}; +use rhai::{Engine, EvalAltResult, ParseError, ParseErrorType}; #[test] #[cfg(not(feature = "no_function"))] @@ -37,7 +37,7 @@ fn test_stack_overflow_parsing() -> Result<(), Box> { engine.compile(r" let a = (1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+1)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) ").expect_err("should error"), - err if err.error_type() == &ParseErrorType::ExprTooDeep + ParseError(x, _) if *x == ParseErrorType::ExprTooDeep )); engine.set_max_expr_depths(100, 6); @@ -71,7 +71,7 @@ fn test_stack_overflow_parsing() -> Result<(), Box> { 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 ").expect_err("should error"), - err if err.error_type() == &ParseErrorType::ExprTooDeep + ParseError(x, _) if *x == ParseErrorType::ExprTooDeep )); #[cfg(not(feature = "no_function"))] From 527577895242ff4e453543bc40c54ff24633a68e Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 12 Jun 2020 18:46:36 +0800 Subject: [PATCH 30/53] Add test to call_fn with String parameter. --- README.md | 2 +- tests/call_fn.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 99ce9018..2b70b36d 100644 --- a/README.md +++ b/README.md @@ -316,7 +316,7 @@ Functions declared with `private` are hidden and cannot be called from Rust (see // Define functions in a script. let ast = engine.compile(true, r#" - // a function with two parameters: String and i64 + // a function with two parameters: string and i64 fn hello(x, y) { x.len + y } diff --git a/tests/call_fn.rs b/tests/call_fn.rs index 0bed8d7b..29f7abca 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -89,6 +89,14 @@ fn test_anonymous_fn() -> Result<(), Box> { assert_eq!(calc_func(42, 123, 9)?, 1485); + let calc_func = Func::<(INT, String, INT), INT>::create_from_script( + Engine::new(), + "fn calc(x, y, z) { (x + len(y)) * z }", + "calc", + )?; + + assert_eq!(calc_func(42, "hello".to_string(), 9)?, 423); + let calc_func = Func::<(INT, INT, INT), INT>::create_from_script( Engine::new(), "private fn calc(x, y, z) { (x + y) * z }", From b24fdd7a4d9b356c5d545065340ecf681a5311f2 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 12 Jun 2020 19:54:55 +0800 Subject: [PATCH 31/53] Change call_fn_dynamic to accept any type that is IntoIterator. --- README.md | 26 ++++++++++++-------------- RELEASES.md | 1 + src/api.rs | 34 +++++++++++++++++++++++----------- src/utils.rs | 9 +++++++++ 4 files changed, 45 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 2b70b36d..7ad9c478 100644 --- a/README.md +++ b/README.md @@ -359,16 +359,14 @@ let result: i64 = engine.call_fn(&mut scope, &ast, "hello", () )?; let result: () = engine.call_fn(&mut scope, &ast, "hidden", ())?; ``` -For more control, construct all arguments as `Dynamic` values and use `Engine::call_fn_dynamic`: +For more control, construct all arguments as `Dynamic` values and use `Engine::call_fn_dynamic`, passing it +anything that implements `IntoIterator` (such as a simple `Vec`): ```rust let result: Dynamic = engine.call_fn_dynamic(&mut scope, &ast, "hello", - &mut [ String::from("abc").into(), 123_i64.into() ])?; + vec![ String::from("abc").into(), 123_i64.into() ])?; ``` -However, beware that `Engine::call_fn_dynamic` _consumes_ its arguments, meaning that all arguments passed to it -will be replaced by `()` afterwards. To re-use the arguments, clone them beforehand and pass in the clone. - ### Creating Rust anonymous functions from Rhai script [`Func`]: #creating-rust-anonymous-functions-from-rhai-script @@ -738,7 +736,7 @@ use rhai::Dynamic; let x = (42_i64).into(); // 'into()' works for standard types -let y = Dynamic::from(String::from("hello!")); // remember &str is not supported by Rhai +let y = Dynamic::from("hello!".to_string()); // remember &str is not supported by Rhai ``` Functions registered with the [`Engine`] can be _overloaded_ as long as the _signature_ is unique, @@ -1124,14 +1122,14 @@ not available under [`no_index`]. To use custom types for `print` and `debug`, or convert its value into a [string], it is necessary that the following functions be registered (assuming the custom type is `T : Display + Debug`): -| Function | Signature | Typical implementation | Usage | -| ----------- | ------------------------------------------------ | ------------------------------ | --------------------------------------------------------------------------------------- | -| `to_string` | `|s: &mut T| -> String` | `s.to_string()` | Converts the custom type into a [string] | -| `print` | `|s: &mut T| -> String` | `s.to_string()` | Converts the custom type into a [string] for the [`print`](#print-and-debug) statement | -| `debug` | `|s: &mut T| -> String` | `format!("{:?}", s)` | Converts the custom type into a [string] for the [`debug`](#print-and-debug) statement | -| `+` | `|s1: ImmutableString, s: T| -> ImmutableString` | `s1 + s` | Append the custom type to another [string], for `print("Answer: " + type);` usage | -| `+` | `|s: T, s2: ImmutableString| -> String` | `s.to_string().push_str(&s2);` | Append another [string] to the custom type, for `print(type + " is the answer");` usage | -| `+=` | `|s1: &mut ImmutableString, s: T|` | `s1 += s.to_string()` | Append the custom type to an existing [string], for `s += type;` usage | +| Function | Signature | Typical implementation | Usage | +| ----------- | ------------------------------------------------ | ------------------------------------- | --------------------------------------------------------------------------------------- | +| `to_string` | `|s: &mut T| -> ImmutableString` | `s.to_string().into()` | Converts the custom type into a [string] | +| `print` | `|s: &mut T| -> ImmutableString` | `s.to_string().into()` | Converts the custom type into a [string] for the [`print`](#print-and-debug) statement | +| `debug` | `|s: &mut T| -> ImmutableString` | `format!("{:?}", s).into()` | Converts the custom type into a [string] for the [`debug`](#print-and-debug) statement | +| `+` | `|s1: ImmutableString, s: T| -> ImmutableString` | `s1 + s` | Append the custom type to another [string], for `print("Answer: " + type);` usage | +| `+` | `|s: T, s2: ImmutableString| -> ImmutableString` | `s.to_string().push_str(&s2).into();` | Append another [string] to the custom type, for `print(type + " is the answer");` usage | +| `+=` | `|s1: &mut ImmutableString, s: T|` | `s1 += s.to_string()` | Append the custom type to an existing [string], for `s += type;` usage | `Scope` - Initializing and maintaining state ------------------------------------------- diff --git a/RELEASES.md b/RELEASES.md index 0b3136c1..6667774c 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -14,6 +14,7 @@ Breaking changes * `Engine::register_indexer` is renamed to `Engine::register_indexer_get`. * `Module::set_indexer_fn` is renamed to `Module::set_indexer_get_fn`. * The tuple `ParseError` now exposes the internal fields and the `ParseError::error_type` and `ParseError::position` methods are removed. The first tuple field is the `ParseErrorType` and the second tuple field is the `Position`. +* `Engine::call_fn_dynamic` now takes any type that implements `IntoIterator`. New features ------------ diff --git a/src/api.rs b/src/api.rs index df92f891..8e2af013 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1114,7 +1114,7 @@ impl Engine { args: A, ) -> Result> { let mut arg_values = args.into_vec(); - let result = self.call_fn_dynamic(scope, ast, name, arg_values.as_mut())?; + let result = self.call_fn_dynamic_raw(scope, ast, name, arg_values.as_mut())?; let return_type = self.map_type_name(result.type_name()); @@ -1128,13 +1128,6 @@ impl Engine { /// Call a script function defined in an `AST` with multiple `Dynamic` arguments. /// - /// ## WARNING - /// - /// All the arguments are _consumed_, meaning that they're replaced by `()`. - /// This is to avoid unnecessarily cloning the arguments. - /// Do you use the arguments after this call. If you need them afterwards, - /// clone them _before_ calling this function. - /// /// # Example /// /// ``` @@ -1155,13 +1148,13 @@ impl Engine { /// scope.push("foo", 42_i64); /// /// // Call the script-defined function - /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add", &mut [ String::from("abc").into(), 123_i64.into() ])?; + /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add", vec![ String::from("abc").into(), 123_i64.into() ])?; /// assert_eq!(result.cast::(), 168); /// - /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add1", &mut [ String::from("abc").into() ])?; + /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add1", vec![ String::from("abc").into() ])?; /// assert_eq!(result.cast::(), 46); /// - /// let result= engine.call_fn_dynamic(&mut scope, &ast, "bar", &mut [])?; + /// let result= engine.call_fn_dynamic(&mut scope, &ast, "bar", vec![])?; /// assert_eq!(result.cast::(), 21); /// # } /// # Ok(()) @@ -1169,6 +1162,25 @@ impl Engine { /// ``` #[cfg(not(feature = "no_function"))] pub fn call_fn_dynamic( + &self, + scope: &mut Scope, + ast: &AST, + name: &str, + arg_values: impl IntoIterator, + ) -> Result> { + let mut arg_values: StaticVec<_> = arg_values.into_iter().collect(); + self.call_fn_dynamic_raw(scope, ast, name, arg_values.as_mut()) + } + + /// Call a script function defined in an `AST` with multiple `Dynamic` arguments. + /// + /// ## WARNING + /// + /// All the arguments are _consumed_, meaning that they're replaced by `()`. + /// This is to avoid unnecessarily cloning the arguments. + /// Do not use the arguments after this call. If they are needed afterwards, + /// clone them _before_ calling this function. + pub(crate) fn call_fn_dynamic_raw( &self, scope: &mut Scope, ast: &AST, diff --git a/src/utils.rs b/src/utils.rs index bedf73b2..77cef0e7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -216,6 +216,15 @@ impl FromIterator for StaticVec { } } +impl IntoIterator for StaticVec { + type Item = T; + type IntoIter = Box>; + + fn into_iter(self) -> Self::IntoIter { + self.into_iter() + } +} + impl StaticVec { /// Create a new `StaticVec`. #[inline(always)] From f00457559094a1513a112aa5f97024c1a0670cd4 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 13 Jun 2020 17:03:49 +0800 Subject: [PATCH 32/53] Fix namespace error in no_std. --- src/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index d75af495..dd1be4ef 100644 --- a/src/error.rs +++ b/src/error.rs @@ -155,7 +155,7 @@ impl ParseErrorType { } impl fmt::Display for ParseErrorType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::BadInput(s) | ParseErrorType::MalformedCallExpr(s) => { write!(f, "{}", if s.is_empty() { self.desc() } else { s }) From 954f971ddf059ae3871a75139b65c40b4ceaa959 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 13 Jun 2020 21:57:46 +0800 Subject: [PATCH 33/53] New optimizer test. --- tests/optimizer.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/optimizer.rs b/tests/optimizer.rs index 49b15c25..828b0da4 100644 --- a/tests/optimizer.rs +++ b/tests/optimizer.rs @@ -3,7 +3,7 @@ use rhai::{Engine, EvalAltResult, OptimizationLevel, INT}; #[test] -fn test_optimizer() -> Result<(), Box> { +fn test_optimizer_run() -> Result<(), Box> { fn run_test(engine: &mut Engine) -> Result<(), Box> { assert_eq!(engine.eval::(r"if true { 42 } else { 123 }")?, 42); assert_eq!( @@ -30,3 +30,27 @@ fn test_optimizer() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_optimizer_parse() -> Result<(), Box> { + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::Simple); + + let ast = engine.compile("{ const DECISION = false; if DECISION { 42 } }")?; + + assert_eq!( + format!("{:?}", ast), + "AST([], )" + ); + + engine.set_optimization_level(OptimizationLevel::Full); + + let ast = engine.compile("if 1 == 2 { 42 }")?; + + assert_eq!( + format!("{:?}", ast), + "AST([], )" + ); + + Ok(()) +} From b690ebac69ec3ddb1562e7f8629ec6158d151a05 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 13 Jun 2020 21:57:57 +0800 Subject: [PATCH 34/53] Refine section on progress tracking. --- README.md | 48 ++++++++++++++++++++++++++++-------------------- _config.yml | 1 - 2 files changed, 28 insertions(+), 21 deletions(-) delete mode 100644 _config.yml diff --git a/README.md b/README.md index 7ad9c478..57c56afa 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,11 @@ Features * Re-entrant scripting [`Engine`] can be made `Send + Sync` (via the [`sync`] feature). * Sand-boxed - the scripting [`Engine`], if declared immutable, cannot mutate the containing environment unless explicitly permitted (e.g. via a `RefCell`). * Rugged (protection against [stack-overflow](#maximum-call-stack-depth) and [runaway scripts](#maximum-number-of-operations) etc.). -* Track script evaluation [progress](#tracking-progress) and manually terminate a script run. +* Track script evaluation [progress](#tracking-progress-and-force-terminate-script-run) and manually terminate a script run. * [`no-std`](#optional-features) support. * [Function overloading](#function-overloading). * [Operator overloading](#operator-overloading). -* Organize code base with dynamically-loadable [Modules]. +* Organize code base with dynamically-loadable [modules]. * Scripts are [optimized](#script-optimization) (useful for template-based machine-generated scripts) for repeated evaluations. * Support for [minimal builds](#minimal-builds) by excluding unneeded language [features](#optional-features). * Very few additional dependencies (right now only [`num-traits`](https://crates.io/crates/num-traits/) @@ -2434,8 +2434,8 @@ engine.set_module_resolver(None); Ruggedization - protect against DoS attacks ------------------------------------------ -For scripting systems open to user-land scripts, it is always best to limit the amount of resources used by a script -so that it does not consume more resources that it is allowed to. +For scripting systems open to untrusted user-land scripts, it is always best to limit the amount of resources used by +a script so that it does not consume more resources that it is allowed to. The most important resources to watch out for are: @@ -2476,38 +2476,46 @@ A good rule-of-thumb is that one simple non-trivial expression consumes on avera One _operation_ can take an unspecified amount of time and real CPU cycles, depending on the particulars. For example, loading a constant consumes very few CPU cycles, while calling an external Rust function, though also counted as only one operation, may consume much more computing resources. -If it helps to visualize, think of an _operation_ as roughly equals to one _instruction_ of a hypothetical CPU. +To help visualize, think of an _operation_ as roughly equals to one _instruction_ of a hypothetical CPU +which includes _specialized_ instructions, such as _function call_, _load module_ etc., each taking up +one CPU cycle to execute. -The _operation count_ is intended to be a very course-grained measurement of the amount of CPU that a script -is consuming, and allows the system to impose a hard upper limit. +The _operations count_ is intended to be a very course-grained measurement of the amount of CPU that a script +has consumed, allowing the system to impose a hard upper limit on computing resources. -A script exceeding the maximum operations count will terminate with an error result. -This check can be disabled via the [`unchecked`] feature for higher performance -(but higher risks as well). +A script exceeding the maximum operations count terminates with an error result. +This can be disabled via the [`unchecked`] feature for higher performance (but higher risks as well). -### Tracking progress +### Tracking progress and force-terminate script run -To track script evaluation progress and to force-terminate a script prematurely (for any reason), -provide a closure to the `Engine::on_progress` method: +It is impossible to know when, or even whether, a script run will end +(a.k.a. the [Halting Problem](http://en.wikipedia.org/wiki/Halting_problem)). +When dealing with third-party untrusted scripts that may be malicious, to track evaluation progress and +to force-terminate a script prematurely (for any reason), provide a closure to the `Engine::on_progress` method: ```rust let mut engine = Engine::new(); -engine.on_progress(|&count| { // 'count' is the number of operations performed +engine.on_progress(|&count| { // parameter is '&u64' - number of operations already performed if count % 1000 == 0 { println!("{}", count); // print out a progress log every 1,000 operations } - true // return 'true' to continue the script - // returning 'false' will terminate the script + true // return 'true' to continue running the script + // return 'false' to immediately terminate the script }); ``` -The closure passed to `Engine::on_progress` will be called once every operation. +The closure passed to `Engine::on_progress` will be called once for every operation. Return `false` to terminate the script immediately. +Notice that the _operations count_ value passed into the closure does not indicate the _percentage_ of work +already done by the script (and thus it is not real _progress_ tracking), because it is impossible to determine +how long a script may run. It is possible, however, to calculate this percentage based on an estimated +total number of operations for a typical run. + ### Maximum number of modules -Rhai by default does not limit how many [modules] are loaded via the [`import`] statement. +Rhai by default does not limit how many [modules] can be loaded via [`import`] statements. This can be changed via the `Engine::set_max_modules` method, with zero being unlimited (the default). ```rust @@ -2528,7 +2536,7 @@ Rhai by default limits function calls to a maximum depth of 128 levels (16 level This limit may be changed via the `Engine::set_max_call_levels` method. When setting this limit, care must be also taken to the evaluation depth of each _statement_ -within the function. It is entirely possible for a malicous script to embed an recursive call deep +within the function. It is entirely possible for a malicous script to embed a recursive call deep inside a nested expression or statement block (see [maximum statement depth](#maximum-statement-depth)). The limit can be disabled via the [`unchecked`] feature for higher performance @@ -2627,7 +2635,7 @@ For example, in the following: ```rust { - let x = 999; // NOT eliminated: Rhai doesn't check yet whether a variable is used later on + let x = 999; // NOT eliminated: variable may be used later on (perhaps even an 'eval') 123; // eliminated: no effect "hello"; // eliminated: no effect [1, 2, x, x*2, 5]; // eliminated: no effect diff --git a/_config.yml b/_config.yml deleted file mode 100644 index c7418817..00000000 --- a/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-slate \ No newline at end of file From 22d30c95c9c80844ffc471f9b4479cea5154c5e3 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 14 Jun 2020 00:09:16 +0800 Subject: [PATCH 35/53] Add maximum data size limits. --- README.md | 86 ++++++++++++++-- RELEASES.md | 1 + src/api.rs | 10 +- src/engine.rs | 97 +++++++++++++++++- src/error.rs | 17 +++- src/parser.rs | 244 ++++++++++++++++++++++++++------------------- src/result.rs | 8 ++ src/token.rs | 58 +++++++---- tests/data_size.rs | 135 +++++++++++++++++++++++++ 9 files changed, 518 insertions(+), 138 deletions(-) create mode 100644 tests/data_size.rs diff --git a/README.md b/README.md index 57c56afa..3ab88d26 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ to add scripting to any application. Features -------- -* Easy-to-use language similar to JS+Rust with dynamic typing but _no_ garbage collector. +* Easy-to-use language similar to JS+Rust with dynamic typing. * Tight integration with native Rust [functions](#working-with-functions) and [types](#custom-types-and-methods), including [getters/setters](#getters-and-setters), [methods](#members-and-methods) and [indexers](#indexers). * Freely pass Rust variables/constants into a script via an external [`Scope`]. @@ -25,7 +25,7 @@ Features one single source file, all with names starting with `"unsafe_"`). * Re-entrant scripting [`Engine`] can be made `Send + Sync` (via the [`sync`] feature). * Sand-boxed - the scripting [`Engine`], if declared immutable, cannot mutate the containing environment unless explicitly permitted (e.g. via a `RefCell`). -* Rugged (protection against [stack-overflow](#maximum-call-stack-depth) and [runaway scripts](#maximum-number-of-operations) etc.). +* Rugged - protection against malicious attacks (such as [stack-overflow](#maximum-call-stack-depth), [over-sized data](#maximum-length-of-strings), and [runaway scripts](#maximum-number-of-operations) etc.) that may come from untrusted third-party user-land scripts. * Track script evaluation [progress](#tracking-progress-and-force-terminate-script-run) and manually terminate a script run. * [`no-std`](#optional-features) support. * [Function overloading](#function-overloading). @@ -1191,13 +1191,16 @@ fn main() -> Result<(), Box> Engine configuration options --------------------------- -| Method | Description | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| `set_optimization_level` | Set the amount of script _optimizations_ performed. See [script optimization]. | -| `set_max_expr_depths` | Set the maximum nesting levels of an expression/statement. See [maximum statement depth](#maximum-statement-depth). | -| `set_max_call_levels` | Set the maximum number of function call levels (default 50) to avoid infinite recursion. See [maximum call stack depth](#maximum-call-stack-depth). | -| `set_max_operations` | Set the maximum number of _operations_ that a script is allowed to consume. See [maximum number of operations](#maximum-number-of-operations). | -| `set_max_modules` | Set the maximum number of [modules] that a script is allowed to load. See [maximum number of modules](#maximum-number-of-modules). | +| Method | Not available under | Description | +| ------------------------ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `set_optimization_level` | [`no_optimize`] | Set the amount of script _optimizations_ performed. See [script optimization]. | +| `set_max_expr_depths` | [`unchecked`] | Set the maximum nesting levels of an expression/statement. See [maximum statement depth](#maximum-statement-depth). | +| `set_max_call_levels` | [`unchecked`] | Set the maximum number of function call levels (default 50) to avoid infinite recursion. See [maximum call stack depth](#maximum-call-stack-depth). | +| `set_max_operations` | [`unchecked`] | Set the maximum number of _operations_ that a script is allowed to consume. See [maximum number of operations](#maximum-number-of-operations). | +| `set_max_modules` | [`unchecked`] | Set the maximum number of [modules] that a script is allowed to load. See [maximum number of modules](#maximum-number-of-modules). | +| `set_max_string_size` | [`unchecked`] | Set the maximum length (in UTF-8 bytes) for [strings]. See [maximum length of strings](#maximum-length-of-strings). | +| `set_max_array_size` | [`unchecked`], [`no_index`] | Set the maximum size for [arrays]. See [maximum size of arrays](#maximum-size-of-arrays). | +| `set_max_map_size` | [`unchecked`], [`no_object`] | Set the maximum number of properties for [object maps]. See [maximum size of object maps](#maximum-size-of-object-maps). | ------- @@ -1498,6 +1501,9 @@ record == "Bob X. Davis: age 42 ❤\n"; 'C' in record == false; ``` +The maximum allowed length of a string can be controlled via `Engine::set_max_string_size` +(see [maximum length of strings](#maximum-length-of-strings)). + ### Built-in functions The following standard methods (mostly defined in the [`MoreStringPackage`](#packages) but excluded if using a [raw `Engine`]) operate on strings: @@ -1673,6 +1679,9 @@ y.len == 0; engine.register_fn("push", |list: &mut Array, item: MyType| list.push(Box::new(item)) ); ``` +The maximum allowed size of an array can be controlled via `Engine::set_max_array_size` +(see [maximum size of arrays](#maximum-size-of-arrays)). + Object maps ----------- @@ -1776,6 +1785,9 @@ y.clear(); // empty the object map y.len() == 0; ``` +The maximum allowed size of an object map can be controlled via `Engine::set_max_map_size` +(see [maximum size of object maps](#maximum-size-of-object-maps)). + ### Parsing from JSON The syntax for an object map is extremely similar to JSON, with the exception of `null` values which can @@ -2439,7 +2451,7 @@ a script so that it does not consume more resources that it is allowed to. The most important resources to watch out for are: -* **Memory**: A malicous script may continuously grow an [array] or [object map] until all memory is consumed. +* **Memory**: A malicous script may continuously grow a [string], an [array] or [object map] until all memory is consumed. It may also create a large [array] or [object map] literal that exhausts all memory during parsing. * **CPU**: A malicous script may run an infinite tight loop that consumes all CPU cycles. * **Time**: A malicous script may run indefinitely, thereby blocking the calling system which is waiting for a result. @@ -2455,6 +2467,60 @@ The most important resources to watch out for are: * **Data**: A malicous script may attempt to read from and/or write to data that it does not own. If this happens, it is a severe security breach and may put the entire system at risk. +### Maximum length of strings + +Rhai by default does not limit how long a [string] can be. +This can be changed via the `Engine::set_max_string_size` method, with zero being unlimited (the default). + +```rust +let mut engine = Engine::new(); + +engine.set_max_string_size(500); // allow strings only up to 500 bytes long (in UTF-8 format) + +engine.set_max_string_size(0); // allow unlimited string length +``` + +A script attempting to create a string literal longer than the maximum will terminate with a parse error. +Any script operation that produces a string longer than the maximum also terminates the script with an error result. +This check can be disabled via the [`unchecked`] feature for higher performance +(but higher risks as well). + +### Maximum size of arrays + +Rhai by default does not limit how large an [array] can be. +This can be changed via the `Engine::set_max_array_size` method, with zero being unlimited (the default). + +```rust +let mut engine = Engine::new(); + +engine.set_max_array_size(500); // allow arrays only up to 500 items + +engine.set_max_array_size(0); // allow unlimited arrays +``` + +A script attempting to create an array literal larger than the maximum will terminate with a parse error. +Any script operation that produces an array larger than the maximum also terminates the script with an error result. +This check can be disabled via the [`unchecked`] feature for higher performance +(but higher risks as well). + +### Maximum size of object maps + +Rhai by default does not limit how large (i.e. the number of properties) an [object map] can be. +This can be changed via the `Engine::set_max_map_size` method, with zero being unlimited (the default). + +```rust +let mut engine = Engine::new(); + +engine.set_max_map_size(500); // allow object maps with only up to 500 properties + +engine.set_max_map_size(0); // allow unlimited object maps +``` + +A script attempting to create an object map literal with more properties than the maximum will terminate with a parse error. +Any script operation that produces an object map with more properties than the maximum also terminates the script with an error result. +This check can be disabled via the [`unchecked`] feature for higher performance +(but higher risks as well). + ### Maximum number of operations Rhai by default does not limit how much time or CPU a script consumes. diff --git a/RELEASES.md b/RELEASES.md index 6667774c..b19caf44 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -21,6 +21,7 @@ New features * Indexers are now split into getters and setters (which now support updates). The API is split into `Engine::register_indexer_get` and `Engine::register_indexer_set` with `Engine::register_indexer_get_set` being a shorthand. Similarly, `Module::set_indexer_get_fn` and `Module::set_indexer_set_fn` are added. * `Engine:register_fn` and `Engine:register_result_fn` accepts functions that take parameters of type `&str` (immutable string slice), which maps directly to `ImmutableString`. This is to avoid needing wrappers for functions taking string parameters. +* Set maximum limit on data sizes: `Engine::set_max_string_size`, `Engine::set_max_array_size` and `Engine::set_max_map_size`. Version 0.15.0 diff --git a/src/api.rs b/src/api.rs index 8e2af013..56adc08c 100644 --- a/src/api.rs +++ b/src/api.rs @@ -547,7 +547,7 @@ impl Engine { scripts: &[&str], optimization_level: OptimizationLevel, ) -> Result { - let stream = lex(scripts); + let stream = lex(scripts, self.max_string_size); self.parse(&mut stream.peekable(), scope, optimization_level) } @@ -669,7 +669,7 @@ impl Engine { // Trims the JSON string and add a '#' in front let scripts = ["#", json.trim()]; - let stream = lex(&scripts); + let stream = lex(&scripts, self.max_string_size); let ast = self.parse_global_expr(&mut stream.peekable(), &scope, OptimizationLevel::None)?; @@ -750,7 +750,7 @@ impl Engine { script: &str, ) -> Result { let scripts = [script]; - let stream = lex(&scripts); + let stream = lex(&scripts, self.max_string_size); { let mut peekable = stream.peekable(); @@ -904,7 +904,7 @@ impl Engine { script: &str, ) -> Result> { let scripts = [script]; - let stream = lex(&scripts); + let stream = lex(&scripts, self.max_string_size); let ast = self.parse_global_expr( &mut stream.peekable(), @@ -1034,7 +1034,7 @@ impl Engine { script: &str, ) -> Result<(), Box> { let scripts = [script]; - let stream = lex(&scripts); + let stream = lex(&scripts, self.max_string_size); let ast = self.parse(&mut stream.peekable(), scope, self.optimization_level)?; self.consume_ast_with_scope(scope, &ast) diff --git a/src/engine.rs b/src/engine.rs index b3d0536d..2ef39ee1 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -67,6 +67,13 @@ pub const MAX_EXPR_DEPTH: usize = usize::MAX; #[cfg(feature = "unchecked")] pub const MAX_FUNCTION_EXPR_DEPTH: usize = usize::MAX; +#[cfg(feature = "unchecked")] +pub const MAX_STRING_SIZE: usize = usize::MAX; +#[cfg(feature = "unchecked")] +pub const MAX_ARRAY_SIZE: usize = usize::MAX; +#[cfg(feature = "unchecked")] +pub const MAX_MAP_SIZE: usize = usize::MAX; + pub const KEYWORD_PRINT: &str = "print"; pub const KEYWORD_DEBUG: &str = "debug"; pub const KEYWORD_TYPE_OF: &str = "type_of"; @@ -262,6 +269,12 @@ pub struct Engine { pub(crate) max_operations: u64, /// Maximum number of modules allowed to load. pub(crate) max_modules: u64, + /// Maximum length of a string. + pub(crate) max_string_size: usize, + /// Maximum length of an array. + pub(crate) max_array_size: usize, + /// Maximum number of properties in a map. + pub(crate) max_map_size: usize, } impl Default for Engine { @@ -298,6 +311,9 @@ impl Default for Engine { max_function_expr_depth: MAX_FUNCTION_EXPR_DEPTH, max_operations: u64::MAX, max_modules: u64::MAX, + max_string_size: usize::MAX, + max_array_size: usize::MAX, + max_map_size: usize::MAX, }; engine.load_package(StandardPackage::new().get()); @@ -442,6 +458,9 @@ impl Engine { max_function_expr_depth: MAX_FUNCTION_EXPR_DEPTH, max_operations: u64::MAX, max_modules: u64::MAX, + max_string_size: usize::MAX, + max_array_size: usize::MAX, + max_map_size: usize::MAX, } } @@ -495,13 +514,33 @@ impl Engine { self.max_modules = if modules == 0 { u64::MAX } else { modules }; } - /// Set the depth limits for expressions/statements. + /// Set the depth limits for expressions/statements (0 for unlimited). #[cfg(not(feature = "unchecked"))] pub fn set_max_expr_depths(&mut self, max_expr_depth: usize, max_function_expr_depth: usize) { self.max_expr_depth = max_expr_depth; self.max_function_expr_depth = max_function_expr_depth; } + /// Set the maximum length of strings (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn set_max_string_size(&mut self, max_size: usize) { + self.max_string_size = max_size; + } + + /// Set the maximum length of arrays (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + #[cfg(not(feature = "no_index"))] + pub fn set_max_array_size(&mut self, max_size: usize) { + self.max_array_size = max_size; + } + + /// Set the maximum length of object maps (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + #[cfg(not(feature = "no_object"))] + pub fn set_max_map_size(&mut self, max_size: usize) { + self.max_map_size = max_size; + } + /// Set the module resolution service used by the `Engine`. /// /// Not available under the `no_module` feature. @@ -1395,7 +1434,7 @@ impl Engine { self.inc_operations(state) .map_err(|err| EvalAltResult::new_position(err, expr.position()))?; - match expr { + let result = match expr { Expr::Expr(x) => self.eval_expr(scope, state, lib, x.as_ref(), level), Expr::IntegerConstant(x) => Ok(x.0.into()), @@ -1731,7 +1770,13 @@ impl Engine { Expr::Unit(_) => Ok(().into()), _ => unreachable!(), + }; + + if let Ok(val) = &result { + self.check_data_size(val)?; } + + result } /// Evaluate a statement @@ -1746,7 +1791,7 @@ impl Engine { self.inc_operations(state) .map_err(|err| EvalAltResult::new_position(err, stmt.position()))?; - match stmt { + let result = match stmt { // No-op Stmt::Noop(_) => Ok(Default::default()), @@ -1998,6 +2043,52 @@ impl Engine { } Ok(Default::default()) } + }; + + if let Ok(val) = &result { + self.check_data_size(val)?; + } + + result + } + + /// Check a `Dynamic` value to ensure that its size is within allowable limit. + fn check_data_size(&self, value: &Dynamic) -> Result<(), Box> { + #[cfg(feature = "unchecked")] + return Ok(()); + + match value { + Dynamic(Union::Str(s)) + if self.max_string_size > 0 && s.len() > self.max_string_size => + { + Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Length of string".to_string(), + self.max_string_size, + s.len(), + Position::none(), + ))) + } + #[cfg(not(feature = "no_index"))] + Dynamic(Union::Array(arr)) + if self.max_array_size > 0 && arr.len() > self.max_array_size => + { + Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Length of array".to_string(), + self.max_array_size, + arr.len(), + Position::none(), + ))) + } + #[cfg(not(feature = "no_object"))] + Dynamic(Union::Map(map)) if self.max_map_size > 0 && map.len() > self.max_map_size => { + Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Number of properties in object map".to_string(), + self.max_map_size, + map.len(), + Position::none(), + ))) + } + _ => Ok(()), } } diff --git a/src/error.rs b/src/error.rs index dd1be4ef..3d4c47c7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,6 +12,8 @@ pub enum LexError { UnexpectedChar(char), /// A string literal is not terminated before a new-line or EOF. UnterminatedString, + /// An identifier is in an invalid format. + StringTooLong(usize), /// An string/character/numeric escape sequence is in an invalid format. MalformedEscapeSequence(String), /// An numeric literal is in an invalid format. @@ -35,6 +37,11 @@ impl fmt::Display for LexError { Self::MalformedChar(s) => write!(f, "Invalid character: '{}'", s), Self::MalformedIdentifier(s) => write!(f, "Variable name is not proper: '{}'", s), Self::UnterminatedString => write!(f, "Open string is not terminated"), + Self::StringTooLong(max) => write!( + f, + "Length of string literal exceeds the maximum limit ({})", + max + ), Self::ImproperKeyword(s) => write!(f, "{}", s), } } @@ -109,12 +116,16 @@ pub enum ParseErrorType { WrongExport, /// Assignment to a copy of a value. AssignmentToCopy, - /// Assignment to an a constant variable. + /// Assignment to an a constant variable. Wrapped value is the constant variable name. AssignmentToConstant(String), /// Expression exceeding the maximum levels of complexity. /// /// Never appears under the `unchecked` feature. ExprTooDeep, + /// Literal exceeding the maximum size. Wrapped values are the data type name and the maximum size. + /// + /// Never appears under the `unchecked` feature. + LiteralTooLarge(String, usize), /// Break statement not inside a loop. LoopBreak, } @@ -149,6 +160,7 @@ impl ParseErrorType { Self::AssignmentToCopy => "Only a copy of the value is change with this assignment", Self::AssignmentToConstant(_) => "Cannot assign to a constant value", Self::ExprTooDeep => "Expression exceeds maximum complexity", + Self::LiteralTooLarge(_, _) => "Literal exceeds maximum limit", Self::LoopBreak => "Break statement should only be used inside a loop" } } @@ -197,6 +209,9 @@ impl fmt::Display for ParseErrorType { Self::AssignmentToConstant(s) if s.is_empty() => write!(f, "{}", self.desc()), Self::AssignmentToConstant(s) => write!(f, "Cannot assign to constant '{}'", s), + Self::LiteralTooLarge(typ, max) => { + write!(f, "{} exceeds the maximum limit ({})", typ, max) + } _ => write!(f, "{}", self.desc()), } } diff --git a/src/parser.rs b/src/parser.rs index 2eed69b1..d17657e0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -201,13 +201,27 @@ struct ParseState { stack: Vec<(String, ScopeEntryType)>, /// Maximum levels of expression nesting. max_expr_depth: usize, + /// Maximum length of a string. + pub max_string_size: usize, + /// Maximum length of an array. + pub max_array_size: usize, + /// Maximum number of properties in a map. + pub max_map_size: usize, } impl ParseState { /// Create a new `ParseState`. - pub fn new(max_expr_depth: usize) -> Self { + pub fn new( + max_expr_depth: usize, + max_string_size: usize, + max_array_size: usize, + max_map_size: usize, + ) -> Self { Self { max_expr_depth, + max_string_size, + max_array_size, + max_map_size, ..Default::default() } } @@ -1070,6 +1084,14 @@ fn parse_array_literal( if !match_token(input, Token::RightBracket)? { while !input.peek().unwrap().0.is_eof() { + if state.max_array_size > 0 && arr.len() >= state.max_array_size { + return Err(PERR::LiteralTooLarge( + "Size of array literal".to_string(), + state.max_array_size, + ) + .into_err(input.peek().unwrap().1)); + } + let expr = parse_expr(input, state, settings.level_up())?; arr.push(expr); @@ -1155,8 +1177,15 @@ fn parse_map_literal( } }; - let expr = parse_expr(input, state, settings.level_up())?; + if state.max_map_size > 0 && map.len() >= state.max_map_size { + return Err(PERR::LiteralTooLarge( + "Number of properties in object map literal".to_string(), + state.max_map_size, + ) + .into_err(input.peek().unwrap().1)); + } + let expr = parse_expr(input, state, settings.level_up())?; map.push(((name, pos), expr)); match input.peek().unwrap() { @@ -2408,102 +2437,6 @@ fn parse_fn( }) } -/// Parse the global level statements. -fn parse_global_level( - input: &mut TokenStream, - max_expr_depth: usize, - max_function_expr_depth: usize, -) -> Result<(Vec, Vec), ParseError> { - let mut statements = Vec::::new(); - let mut functions = HashMap::::with_hasher(StraightHasherBuilder); - let mut state = ParseState::new(max_expr_depth); - - while !input.peek().unwrap().0.is_eof() { - // Collect all the function definitions - #[cfg(not(feature = "no_function"))] - { - let (access, must_be_fn) = if match_token(input, Token::Private)? { - (FnAccess::Private, true) - } else { - (FnAccess::Public, false) - }; - - match input.peek().unwrap() { - #[cfg(not(feature = "no_function"))] - (Token::Fn, pos) => { - let mut state = ParseState::new(max_function_expr_depth); - let settings = ParseSettings { - allow_if_expr: true, - allow_stmt_expr: true, - is_global: false, - is_breakable: false, - level: 0, - pos: *pos, - }; - let func = parse_fn(input, &mut state, access, settings)?; - - // Qualifiers (none) + function name + number of arguments. - let hash = calc_fn_hash(empty(), &func.name, func.params.len(), empty()); - - functions.insert(hash, func); - continue; - } - (_, pos) if must_be_fn => { - return Err(PERR::MissingToken( - Token::Fn.into(), - format!("following '{}'", Token::Private.syntax()), - ) - .into_err(*pos)) - } - _ => (), - } - } - - // Actual statement - let settings = ParseSettings { - allow_if_expr: true, - allow_stmt_expr: true, - is_global: true, - is_breakable: false, - level: 0, - pos: Position::none(), - }; - let stmt = parse_stmt(input, &mut state, settings)?; - - let need_semicolon = !stmt.is_self_terminated(); - - statements.push(stmt); - - match input.peek().unwrap() { - // EOF - (Token::EOF, _) => break, - // stmt ; - (Token::SemiColon, _) if need_semicolon => { - eat_token(input, Token::SemiColon); - } - // stmt ; - (Token::SemiColon, _) if !need_semicolon => (), - // { stmt } ??? - (_, _) if !need_semicolon => (), - // stmt - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(*pos)) - } - // stmt ??? - (_, pos) => { - // Semicolons are not optional between statements - return Err(PERR::MissingToken( - Token::SemiColon.into(), - "to terminate this statement".into(), - ) - .into_err(*pos)); - } - } - } - - Ok((statements, functions.into_iter().map(|(_, v)| v).collect())) -} - impl Engine { pub(crate) fn parse_global_expr( &self, @@ -2511,7 +2444,12 @@ impl Engine { scope: &Scope, optimization_level: OptimizationLevel, ) -> Result { - let mut state = ParseState::new(self.max_expr_depth); + let mut state = ParseState::new( + self.max_expr_depth, + self.max_string_size, + self.max_array_size, + self.max_map_size, + ); let settings = ParseSettings { allow_if_expr: false, allow_stmt_expr: false, @@ -2540,6 +2478,111 @@ impl Engine { ) } + /// Parse the global level statements. + fn parse_global_level( + &self, + input: &mut TokenStream, + ) -> Result<(Vec, Vec), ParseError> { + let mut statements = Vec::::new(); + let mut functions = HashMap::::with_hasher(StraightHasherBuilder); + let mut state = ParseState::new( + self.max_expr_depth, + self.max_string_size, + self.max_array_size, + self.max_map_size, + ); + + while !input.peek().unwrap().0.is_eof() { + // Collect all the function definitions + #[cfg(not(feature = "no_function"))] + { + let (access, must_be_fn) = if match_token(input, Token::Private)? { + (FnAccess::Private, true) + } else { + (FnAccess::Public, false) + }; + + match input.peek().unwrap() { + #[cfg(not(feature = "no_function"))] + (Token::Fn, pos) => { + let mut state = ParseState::new( + self.max_function_expr_depth, + self.max_string_size, + self.max_array_size, + self.max_map_size, + ); + let settings = ParseSettings { + allow_if_expr: true, + allow_stmt_expr: true, + is_global: false, + is_breakable: false, + level: 0, + pos: *pos, + }; + let func = parse_fn(input, &mut state, access, settings)?; + + // Qualifiers (none) + function name + number of arguments. + let hash = calc_fn_hash(empty(), &func.name, func.params.len(), empty()); + + functions.insert(hash, func); + continue; + } + (_, pos) if must_be_fn => { + return Err(PERR::MissingToken( + Token::Fn.into(), + format!("following '{}'", Token::Private.syntax()), + ) + .into_err(*pos)) + } + _ => (), + } + } + + // Actual statement + let settings = ParseSettings { + allow_if_expr: true, + allow_stmt_expr: true, + is_global: true, + is_breakable: false, + level: 0, + pos: Position::none(), + }; + let stmt = parse_stmt(input, &mut state, settings)?; + + let need_semicolon = !stmt.is_self_terminated(); + + statements.push(stmt); + + match input.peek().unwrap() { + // EOF + (Token::EOF, _) => break, + // stmt ; + (Token::SemiColon, _) if need_semicolon => { + eat_token(input, Token::SemiColon); + } + // stmt ; + (Token::SemiColon, _) if !need_semicolon => (), + // { stmt } ??? + (_, _) if !need_semicolon => (), + // stmt + (Token::LexError(err), pos) => { + return Err(PERR::BadInput(err.to_string()).into_err(*pos)) + } + // stmt ??? + (_, pos) => { + // Semicolons are not optional between statements + return Err(PERR::MissingToken( + Token::SemiColon.into(), + "to terminate this statement".into(), + ) + .into_err(*pos)); + } + } + } + + Ok((statements, functions.into_iter().map(|(_, v)| v).collect())) + } + /// Run the parser on an input stream, returning an AST. pub(crate) fn parse( &self, @@ -2547,8 +2590,7 @@ impl Engine { scope: &Scope, optimization_level: OptimizationLevel, ) -> Result { - let (statements, lib) = - parse_global_level(input, self.max_expr_depth, self.max_function_expr_depth)?; + let (statements, lib) = self.parse_global_level(input)?; Ok( // Optimize AST diff --git a/src/result.rs b/src/result.rs index 36bc18f6..83943df2 100644 --- a/src/result.rs +++ b/src/result.rs @@ -81,6 +81,8 @@ pub enum EvalAltResult { ErrorTooManyModules(Position), /// Call stack over maximum limit. ErrorStackOverflow(Position), + /// Data value over maximum size limit. Wrapped values are the data type, maximum size and current size. + ErrorDataTooLarge(String, usize, usize, Position), /// The script is prematurely terminated. ErrorTerminated(Position), /// Run-time error encountered. Wrapped value is the error message. @@ -139,6 +141,7 @@ impl EvalAltResult { Self::ErrorTooManyOperations(_) => "Too many operations", Self::ErrorTooManyModules(_) => "Too many modules imported", Self::ErrorStackOverflow(_) => "Stack overflow", + Self::ErrorDataTooLarge(_, _, _, _) => "Data size exceeds maximum limit", Self::ErrorTerminated(_) => "Script terminated.", Self::ErrorRuntime(_, _) => "Runtime error", Self::ErrorLoopBreak(true, _) => "Break statement not inside a loop", @@ -228,6 +231,9 @@ impl fmt::Display for EvalAltResult { "String index {} is out of bounds: only {} characters in the string", index, max )?, + Self::ErrorDataTooLarge(typ, max, size, _) => { + write!(f, "{} ({}) exceeds the maximum limit ({})", typ, size, max)? + } } // Do not write any position if None @@ -279,6 +285,7 @@ impl EvalAltResult { | Self::ErrorTooManyOperations(pos) | Self::ErrorTooManyModules(pos) | Self::ErrorStackOverflow(pos) + | Self::ErrorDataTooLarge(_, _, _, pos) | Self::ErrorTerminated(pos) | Self::ErrorRuntime(_, pos) | Self::ErrorLoopBreak(_, pos) @@ -316,6 +323,7 @@ impl EvalAltResult { | Self::ErrorTooManyOperations(pos) | Self::ErrorTooManyModules(pos) | Self::ErrorStackOverflow(pos) + | Self::ErrorDataTooLarge(_, _, _, pos) | Self::ErrorTerminated(pos) | Self::ErrorRuntime(_, pos) | Self::ErrorLoopBreak(_, pos) diff --git a/src/token.rs b/src/token.rs index d6799a6f..614783b4 100644 --- a/src/token.rs +++ b/src/token.rs @@ -429,6 +429,8 @@ impl From for String { /// An iterator on a `Token` stream. pub struct TokenIterator<'a> { + /// Maximum length of a string (0 = unlimited). + max_string_size: usize, /// Can the next token be a unary operator? can_be_unary: bool, /// Current position. @@ -494,6 +496,7 @@ impl<'a> TokenIterator<'a> { pub fn parse_string_literal( &mut self, enclosing_char: char, + max_length: usize, ) -> Result { let mut result = Vec::new(); let mut escape = String::with_capacity(12); @@ -505,6 +508,10 @@ impl<'a> TokenIterator<'a> { self.advance(); + if max_length > 0 && result.len() > max_length { + return Err((LexError::StringTooLong(max_length), self.pos)); + } + match next_char { // \... '\\' if escape.is_empty() => { @@ -592,7 +599,13 @@ impl<'a> TokenIterator<'a> { } } - Ok(result.iter().collect()) + let s = result.iter().collect::(); + + if max_length > 0 && s.len() > max_length { + return Err((LexError::StringTooLong(max_length), self.pos)); + } + + Ok(s) } /// Get the next token. @@ -779,10 +792,12 @@ impl<'a> TokenIterator<'a> { // " - string literal ('"', _) => { - return self.parse_string_literal('"').map_or_else( - |err| Some((Token::LexError(Box::new(err.0)), err.1)), - |out| Some((Token::StringConst(out), pos)), - ); + return self + .parse_string_literal('"', self.max_string_size) + .map_or_else( + |err| Some((Token::LexError(Box::new(err.0)), err.1)), + |out| Some((Token::StringConst(out), pos)), + ); } // ' - character literal @@ -793,19 +808,25 @@ impl<'a> TokenIterator<'a> { )); } ('\'', _) => { - return Some(self.parse_string_literal('\'').map_or_else( - |err| (Token::LexError(Box::new(err.0)), err.1), - |result| { - let mut chars = result.chars(); - let first = chars.next(); + return Some( + self.parse_string_literal('\'', self.max_string_size) + .map_or_else( + |err| (Token::LexError(Box::new(err.0)), err.1), + |result| { + let mut chars = result.chars(); + let first = chars.next(); - if chars.next().is_some() { - (Token::LexError(Box::new(LERR::MalformedChar(result))), pos) - } else { - (Token::CharConstant(first.expect("should be Some")), pos) - } - }, - )); + if chars.next().is_some() { + ( + Token::LexError(Box::new(LERR::MalformedChar(result))), + pos, + ) + } else { + (Token::CharConstant(first.expect("should be Some")), pos) + } + }, + ), + ); } // Braces @@ -1047,8 +1068,9 @@ impl<'a> Iterator for TokenIterator<'a> { } /// Tokenize an input text stream. -pub fn lex<'a>(input: &'a [&'a str]) -> TokenIterator<'a> { +pub fn lex<'a>(input: &'a [&'a str], max_string_size: usize) -> TokenIterator<'a> { TokenIterator { + max_string_size, can_be_unary: true, pos: Position::new(1, 0), streams: input.iter().map(|s| s.chars().peekable()).collect(), diff --git a/tests/data_size.rs b/tests/data_size.rs new file mode 100644 index 00000000..637fde5e --- /dev/null +++ b/tests/data_size.rs @@ -0,0 +1,135 @@ +#![cfg(not(feature = "unchecked"))] +use rhai::{Engine, EvalAltResult, ParseError, ParseErrorType}; + +#[cfg(not(feature = "no_index"))] +use rhai::Array; + +#[cfg(not(feature = "no_object"))] +use rhai::Map; + +#[test] +fn test_max_string_size() -> Result<(), Box> { + let mut engine = Engine::new(); + engine.set_max_string_size(10); + + assert!(matches!( + engine.compile(r#"let x = "hello, world!";"#).expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::BadInput("Length of string literal exceeds the maximum limit (10)".to_string()) + )); + + assert!(matches!( + *engine + .eval::( + r#" + let x = "hello, "; + let y = "world!"; + x + y + "# + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 13, _) + )); + + engine.set_max_string_size(0); + + assert_eq!( + engine.eval::( + r#" + let x = "hello, "; + let y = "world!"; + x + y + "# + )?, + "hello, world!" + ); + + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_index"))] +fn test_max_array_size() -> Result<(), Box> { + let mut engine = Engine::new(); + engine.set_max_array_size(10); + + assert!(matches!( + engine + .compile("let x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];") + .expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::LiteralTooLarge("Size of array literal".to_string(), 10) + )); + + assert!(matches!( + *engine + .eval::( + r" + let x = [1,2,3,4,5,6]; + let y = [7,8,9,10,11,12]; + x + y + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) + )); + + engine.set_max_array_size(0); + + assert_eq!( + engine + .eval::( + r" + let x = [1,2,3,4,5,6]; + let y = [7,8,9,10,11,12]; + x + y + " + )? + .len(), + 12 + ); + + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_object"))] +fn test_max_map_size() -> Result<(), Box> { + let mut engine = Engine::new(); + engine.set_max_map_size(10); + + assert!(matches!( + engine + .compile("let x = #{a:1,b:2,c:3,d:4,e:5,f:6,g:7,h:8,i:9,j:10,k:11,l:12,m:13,n:14,o:15};") + .expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::LiteralTooLarge("Number of properties in object map literal".to_string(), 10) + )); + + assert!(matches!( + *engine + .eval::( + r" + let x = #{a:1,b:2,c:3,d:4,e:5,f:6}; + let y = #{g:7,h:8,i:9,j:10,k:11,l:12}; + x + y + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) + )); + + engine.set_max_map_size(0); + + assert_eq!( + engine + .eval::( + r" + let x = #{a:1,b:2,c:3,d:4,e:5,f:6}; + let y = #{g:7,h:8,i:9,j:10,k:11,l:12}; + x + y + " + )? + .len(), + 12 + ); + + Ok(()) +} From bb691a03138b0a9f920e765650836c9ec2d70d7c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 14 Jun 2020 14:25:47 +0800 Subject: [PATCH 36/53] Add maximum data size limits. --- README.md | 122 ++++++++++++++++++--- RELEASES.md | 1 + src/api.rs | 10 +- src/engine.rs | 162 ++++++++++++++++++++++++---- src/error.rs | 17 ++- src/parser.rs | 254 ++++++++++++++++++++++++++------------------ src/result.rs | 8 ++ src/token.rs | 58 ++++++---- tests/data_size.rs | 234 ++++++++++++++++++++++++++++++++++++++++ tests/modules.rs | 2 +- tests/operations.rs | 17 +++ 11 files changed, 725 insertions(+), 160 deletions(-) create mode 100644 tests/data_size.rs diff --git a/README.md b/README.md index 57c56afa..c0931fa6 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ to add scripting to any application. Features -------- -* Easy-to-use language similar to JS+Rust with dynamic typing but _no_ garbage collector. +* Easy-to-use language similar to JS+Rust with dynamic typing. * Tight integration with native Rust [functions](#working-with-functions) and [types](#custom-types-and-methods), including [getters/setters](#getters-and-setters), [methods](#members-and-methods) and [indexers](#indexers). * Freely pass Rust variables/constants into a script via an external [`Scope`]. @@ -25,7 +25,7 @@ Features one single source file, all with names starting with `"unsafe_"`). * Re-entrant scripting [`Engine`] can be made `Send + Sync` (via the [`sync`] feature). * Sand-boxed - the scripting [`Engine`], if declared immutable, cannot mutate the containing environment unless explicitly permitted (e.g. via a `RefCell`). -* Rugged (protection against [stack-overflow](#maximum-call-stack-depth) and [runaway scripts](#maximum-number-of-operations) etc.). +* Rugged - protection against malicious attacks (such as [stack-overflow](#maximum-call-stack-depth), [over-sized data](#maximum-length-of-strings), and [runaway scripts](#maximum-number-of-operations) etc.) that may come from untrusted third-party user-land scripts. * Track script evaluation [progress](#tracking-progress-and-force-terminate-script-run) and manually terminate a script run. * [`no-std`](#optional-features) support. * [Function overloading](#function-overloading). @@ -1191,13 +1191,16 @@ fn main() -> Result<(), Box> Engine configuration options --------------------------- -| Method | Description | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| `set_optimization_level` | Set the amount of script _optimizations_ performed. See [script optimization]. | -| `set_max_expr_depths` | Set the maximum nesting levels of an expression/statement. See [maximum statement depth](#maximum-statement-depth). | -| `set_max_call_levels` | Set the maximum number of function call levels (default 50) to avoid infinite recursion. See [maximum call stack depth](#maximum-call-stack-depth). | -| `set_max_operations` | Set the maximum number of _operations_ that a script is allowed to consume. See [maximum number of operations](#maximum-number-of-operations). | -| `set_max_modules` | Set the maximum number of [modules] that a script is allowed to load. See [maximum number of modules](#maximum-number-of-modules). | +| Method | Not available under | Description | +| ------------------------ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `set_optimization_level` | [`no_optimize`] | Set the amount of script _optimizations_ performed. See [script optimization]. | +| `set_max_expr_depths` | [`unchecked`] | Set the maximum nesting levels of an expression/statement. See [maximum statement depth](#maximum-statement-depth). | +| `set_max_call_levels` | [`unchecked`] | Set the maximum number of function call levels (default 50) to avoid infinite recursion. See [maximum call stack depth](#maximum-call-stack-depth). | +| `set_max_operations` | [`unchecked`] | Set the maximum number of _operations_ that a script is allowed to consume. See [maximum number of operations](#maximum-number-of-operations). | +| `set_max_modules` | [`unchecked`] | Set the maximum number of [modules] that a script is allowed to load. See [maximum number of modules](#maximum-number-of-modules). | +| `set_max_string_size` | [`unchecked`] | Set the maximum length (in UTF-8 bytes) for [strings]. See [maximum length of strings](#maximum-length-of-strings). | +| `set_max_array_size` | [`unchecked`], [`no_index`] | Set the maximum size for [arrays]. See [maximum size of arrays](#maximum-size-of-arrays). | +| `set_max_map_size` | [`unchecked`], [`no_object`] | Set the maximum number of properties for [object maps]. See [maximum size of object maps](#maximum-size-of-object-maps). | ------- @@ -1498,6 +1501,9 @@ record == "Bob X. Davis: age 42 ❤\n"; 'C' in record == false; ``` +The maximum allowed length of a string can be controlled via `Engine::set_max_string_size` +(see [maximum length of strings](#maximum-length-of-strings)). + ### Built-in functions The following standard methods (mostly defined in the [`MoreStringPackage`](#packages) but excluded if using a [raw `Engine`]) operate on strings: @@ -1673,6 +1679,9 @@ y.len == 0; engine.register_fn("push", |list: &mut Array, item: MyType| list.push(Box::new(item)) ); ``` +The maximum allowed size of an array can be controlled via `Engine::set_max_array_size` +(see [maximum size of arrays](#maximum-size-of-arrays)). + Object maps ----------- @@ -1776,6 +1785,9 @@ y.clear(); // empty the object map y.len() == 0; ``` +The maximum allowed size of an object map can be controlled via `Engine::set_max_map_size` +(see [maximum size of object maps](#maximum-size-of-object-maps)). + ### Parsing from JSON The syntax for an object map is extremely similar to JSON, with the exception of `null` values which can @@ -2439,7 +2451,7 @@ a script so that it does not consume more resources that it is allowed to. The most important resources to watch out for are: -* **Memory**: A malicous script may continuously grow an [array] or [object map] until all memory is consumed. +* **Memory**: A malicous script may continuously grow a [string], an [array] or [object map] until all memory is consumed. It may also create a large [array] or [object map] literal that exhausts all memory during parsing. * **CPU**: A malicous script may run an infinite tight loop that consumes all CPU cycles. * **Time**: A malicous script may run indefinitely, thereby blocking the calling system which is waiting for a result. @@ -2455,6 +2467,89 @@ The most important resources to watch out for are: * **Data**: A malicous script may attempt to read from and/or write to data that it does not own. If this happens, it is a severe security breach and may put the entire system at risk. +### Maximum length of strings + +Rhai by default does not limit how long a [string] can be. +This can be changed via the `Engine::set_max_string_size` method, with zero being unlimited (the default). + +```rust +let mut engine = Engine::new(); + +engine.set_max_string_size(500); // allow strings only up to 500 bytes long (in UTF-8 format) + +engine.set_max_string_size(0); // allow unlimited string length +``` + +A script attempting to create a string literal longer than the maximum will terminate with a parse error. +Any script operation that produces a string longer than the maximum also terminates the script with an error result. +This check can be disabled via the [`unchecked`] feature for higher performance +(but higher risks as well). + +Be conservative when setting a maximum limit and always consider the fact that a registered function may grow +a string's length without Rhai noticing until the very end. For instance, the built-in '`+`' operator for strings +concatenates two strings together to form one longer string; if both strings are _slightly_ below the maximum +length limit, the resultant string may be almost _twice_ the maximum length. The '`pad`' function grows a string +to a specified length which may be longer than the allowed maximum +(to trap this risk, register a custom '`pad`' function that checks the arguments). + +### Maximum size of arrays + +Rhai by default does not limit how large an [array] can be. +This can be changed via the `Engine::set_max_array_size` method, with zero being unlimited (the default). + +```rust +let mut engine = Engine::new(); + +engine.set_max_array_size(500); // allow arrays only up to 500 items + +engine.set_max_array_size(0); // allow unlimited arrays +``` + +A script attempting to create an array literal larger than the maximum will terminate with a parse error. +Any script operation that produces an array larger than the maximum also terminates the script with an error result. +This check can be disabled via the [`unchecked`] feature for higher performance +(but higher risks as well). + +Be conservative when setting a maximum limit and always consider the fact that a registered function may grow +an array's size without Rhai noticing until the very end. +For instance, the built-in '`+`' operator for arrays concatenates two arrays together to form one larger array; +if both arrays are _slightly_ below the maximum size limit, the resultant array may be almost _twice_ the maximum size. +The '`pad`' function grows an array to a specified size which may be larger than the allowed maximum +(to trap this risk, register a custom '`pad`' function that checks the arguments). + +As a malicious script may create a deeply-nested array which consumes huge amounts of memory while each individual +array still stays under the maximum size limit, Rhai also recursively adds up the sizes of all strings, arrays +and object maps contained within each array to make sure that the _aggregate_ sizes of none of these data structures +exceed their respective maximum size limits (if any). + +### Maximum size of object maps + +Rhai by default does not limit how large (i.e. the number of properties) an [object map] can be. +This can be changed via the `Engine::set_max_map_size` method, with zero being unlimited (the default). + +```rust +let mut engine = Engine::new(); + +engine.set_max_map_size(500); // allow object maps with only up to 500 properties + +engine.set_max_map_size(0); // allow unlimited object maps +``` + +A script attempting to create an object map literal with more properties than the maximum will terminate with a parse error. +Any script operation that produces an object map with more properties than the maximum also terminates the script with an error result. +This check can be disabled via the [`unchecked`] feature for higher performance +(but higher risks as well). + +Be conservative when setting a maximum limit and always consider the fact that a registered function may grow +an object map's size without Rhai noticing until the very end. For instance, the built-in '`+`' operator for object maps +concatenates two object maps together to form one larger object map; if both object maps are _slightly_ below the maximum +size limit, the resultant object map may be almost _twice_ the maximum size. + +As a malicious script may create a deeply-nested object map which consumes huge amounts of memory while each individual +object map still stays under the maximum size limit, Rhai also recursively adds up the sizes of all strings, arrays +and object maps contained within each object map to make sure that the _aggregate_ sizes of none of these data structures +exceed their respective maximum size limits (if any). + ### Maximum number of operations Rhai by default does not limit how much time or CPU a script consumes. @@ -2516,14 +2611,17 @@ total number of operations for a typical run. ### Maximum number of modules Rhai by default does not limit how many [modules] can be loaded via [`import`] statements. -This can be changed via the `Engine::set_max_modules` method, with zero being unlimited (the default). +This can be changed via the `Engine::set_max_modules` method. Notice that setting the maximum number +of modules to zero does _not_ indicate unlimited modules, but disallows loading any module altogether. ```rust let mut engine = Engine::new(); engine.set_max_modules(5); // allow loading only up to 5 modules -engine.set_max_modules(0); // allow unlimited modules +engine.set_max_modules(0); // disallow loading any module (maximum = zero) + +engine.set_max_modules(1000); // set to a large number for effectively unlimited modules ``` A script attempting to load more than the maximum number of modules will terminate with an error result. diff --git a/RELEASES.md b/RELEASES.md index 6667774c..b19caf44 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -21,6 +21,7 @@ New features * Indexers are now split into getters and setters (which now support updates). The API is split into `Engine::register_indexer_get` and `Engine::register_indexer_set` with `Engine::register_indexer_get_set` being a shorthand. Similarly, `Module::set_indexer_get_fn` and `Module::set_indexer_set_fn` are added. * `Engine:register_fn` and `Engine:register_result_fn` accepts functions that take parameters of type `&str` (immutable string slice), which maps directly to `ImmutableString`. This is to avoid needing wrappers for functions taking string parameters. +* Set maximum limit on data sizes: `Engine::set_max_string_size`, `Engine::set_max_array_size` and `Engine::set_max_map_size`. Version 0.15.0 diff --git a/src/api.rs b/src/api.rs index 8e2af013..56adc08c 100644 --- a/src/api.rs +++ b/src/api.rs @@ -547,7 +547,7 @@ impl Engine { scripts: &[&str], optimization_level: OptimizationLevel, ) -> Result { - let stream = lex(scripts); + let stream = lex(scripts, self.max_string_size); self.parse(&mut stream.peekable(), scope, optimization_level) } @@ -669,7 +669,7 @@ impl Engine { // Trims the JSON string and add a '#' in front let scripts = ["#", json.trim()]; - let stream = lex(&scripts); + let stream = lex(&scripts, self.max_string_size); let ast = self.parse_global_expr(&mut stream.peekable(), &scope, OptimizationLevel::None)?; @@ -750,7 +750,7 @@ impl Engine { script: &str, ) -> Result { let scripts = [script]; - let stream = lex(&scripts); + let stream = lex(&scripts, self.max_string_size); { let mut peekable = stream.peekable(); @@ -904,7 +904,7 @@ impl Engine { script: &str, ) -> Result> { let scripts = [script]; - let stream = lex(&scripts); + let stream = lex(&scripts, self.max_string_size); let ast = self.parse_global_expr( &mut stream.peekable(), @@ -1034,7 +1034,7 @@ impl Engine { script: &str, ) -> Result<(), Box> { let scripts = [script]; - let stream = lex(&scripts); + let stream = lex(&scripts, self.max_string_size); let ast = self.parse(&mut stream.peekable(), scope, self.optimization_level)?; self.consume_ast_with_scope(scope, &ast) diff --git a/src/engine.rs b/src/engine.rs index b3d0536d..83878c3c 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -182,7 +182,7 @@ pub struct State { /// Number of operations performed. pub operations: u64, /// Number of modules loaded. - pub modules: u64, + pub modules: usize, } impl State { @@ -261,7 +261,13 @@ pub struct Engine { /// Maximum number of operations allowed to run. pub(crate) max_operations: u64, /// Maximum number of modules allowed to load. - pub(crate) max_modules: u64, + pub(crate) max_modules: usize, + /// Maximum length of a string. + pub(crate) max_string_size: usize, + /// Maximum length of an array. + pub(crate) max_array_size: usize, + /// Maximum number of properties in a map. + pub(crate) max_map_size: usize, } impl Default for Engine { @@ -296,8 +302,11 @@ impl Default for Engine { max_call_stack_depth: MAX_CALL_STACK_DEPTH, max_expr_depth: MAX_EXPR_DEPTH, max_function_expr_depth: MAX_FUNCTION_EXPR_DEPTH, - max_operations: u64::MAX, - max_modules: u64::MAX, + max_operations: 0, + max_modules: usize::MAX, + max_string_size: 0, + max_array_size: 0, + max_map_size: 0, }; engine.load_package(StandardPackage::new().get()); @@ -440,8 +449,11 @@ impl Engine { max_call_stack_depth: MAX_CALL_STACK_DEPTH, max_expr_depth: MAX_EXPR_DEPTH, max_function_expr_depth: MAX_FUNCTION_EXPR_DEPTH, - max_operations: u64::MAX, - max_modules: u64::MAX, + max_operations: 0, + max_modules: usize::MAX, + max_string_size: 0, + max_array_size: 0, + max_map_size: 0, } } @@ -482,26 +494,42 @@ impl Engine { /// consuming too much resources (0 for unlimited). #[cfg(not(feature = "unchecked"))] pub fn set_max_operations(&mut self, operations: u64) { - self.max_operations = if operations == 0 { - u64::MAX - } else { - operations - }; + self.max_operations = operations; } - /// Set the maximum number of imported modules allowed for a script (0 for unlimited). + /// Set the maximum number of imported modules allowed for a script. #[cfg(not(feature = "unchecked"))] - pub fn set_max_modules(&mut self, modules: u64) { - self.max_modules = if modules == 0 { u64::MAX } else { modules }; + pub fn set_max_modules(&mut self, modules: usize) { + self.max_modules = modules; } - /// Set the depth limits for expressions/statements. + /// Set the depth limits for expressions/statements (0 for unlimited). #[cfg(not(feature = "unchecked"))] pub fn set_max_expr_depths(&mut self, max_expr_depth: usize, max_function_expr_depth: usize) { self.max_expr_depth = max_expr_depth; self.max_function_expr_depth = max_function_expr_depth; } + /// Set the maximum length of strings (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn set_max_string_size(&mut self, max_size: usize) { + self.max_string_size = max_size; + } + + /// Set the maximum length of arrays (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + #[cfg(not(feature = "no_index"))] + pub fn set_max_array_size(&mut self, max_size: usize) { + self.max_array_size = max_size; + } + + /// Set the maximum length of object maps (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + #[cfg(not(feature = "no_object"))] + pub fn set_max_map_size(&mut self, max_size: usize) { + self.max_map_size = max_size; + } + /// Set the module resolution service used by the `Engine`. /// /// Not available under the `no_module` feature. @@ -1395,7 +1423,7 @@ impl Engine { self.inc_operations(state) .map_err(|err| EvalAltResult::new_position(err, expr.position()))?; - match expr { + let result = match expr { Expr::Expr(x) => self.eval_expr(scope, state, lib, x.as_ref(), level), Expr::IntegerConstant(x) => Ok(x.0.into()), @@ -1731,7 +1759,13 @@ impl Engine { Expr::Unit(_) => Ok(().into()), _ => unreachable!(), + }; + + if let Ok(val) = &result { + self.check_data_size(val)?; } + + result } /// Evaluate a statement @@ -1746,7 +1780,7 @@ impl Engine { self.inc_operations(state) .map_err(|err| EvalAltResult::new_position(err, stmt.position()))?; - match stmt { + let result = match stmt { // No-op Stmt::Noop(_) => Ok(Default::default()), @@ -1998,6 +2032,98 @@ impl Engine { } Ok(Default::default()) } + }; + + if let Ok(val) = &result { + self.check_data_size(val)?; + } + + result + } + + /// Check a `Dynamic` value to ensure that its size is within allowable limit. + fn check_data_size(&self, value: &Dynamic) -> Result<(), Box> { + #[cfg(feature = "unchecked")] + return Ok(()); + + if self.max_string_size + self.max_array_size + self.max_map_size == 0 { + return Ok(()); + } + + // Recursively calculate the size of a value (especially `Array` and `Map`) + fn calc_size(value: &Dynamic) -> (usize, usize, usize) { + match value { + #[cfg(not(feature = "no_index"))] + Dynamic(Union::Array(arr)) => { + let mut arrays = 0; + let mut maps = 0; + + arr.iter().for_each(|value| match value { + Dynamic(Union::Array(_)) | Dynamic(Union::Map(_)) => { + let (a, m, _) = calc_size(value); + arrays += a; + maps += m; + } + _ => arrays += 1, + }); + + (arrays, maps, 0) + } + #[cfg(not(feature = "no_object"))] + Dynamic(Union::Map(map)) => { + let mut arrays = 0; + let mut maps = 0; + + map.values().for_each(|value| match value { + Dynamic(Union::Array(_)) | Dynamic(Union::Map(_)) => { + let (a, m, _) = calc_size(value); + arrays += a; + maps += m; + } + _ => maps += 1, + }); + + (arrays, maps, 0) + } + Dynamic(Union::Str(s)) => (0, 0, s.len()), + _ => (0, 0, 0), + } + } + + match value { + Dynamic(Union::Str(_)) if self.max_string_size > 0 => (), + #[cfg(not(feature = "no_index"))] + Dynamic(Union::Array(_)) if self.max_array_size > 0 => (), + #[cfg(not(feature = "no_object"))] + Dynamic(Union::Map(_)) if self.max_map_size > 0 => (), + _ => return Ok(()), + }; + + let (arr, map, s) = calc_size(value); + + if s > self.max_string_size { + Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Length of string".to_string(), + self.max_string_size, + s, + Position::none(), + ))) + } else if arr > self.max_array_size { + Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Length of array".to_string(), + self.max_array_size, + arr, + Position::none(), + ))) + } else if map > self.max_map_size { + Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Number of properties in object map".to_string(), + self.max_map_size, + map, + Position::none(), + ))) + } else { + Ok(()) } } @@ -2009,7 +2135,7 @@ impl Engine { #[cfg(not(feature = "unchecked"))] { // Guard against too many operations - if state.operations > self.max_operations { + if self.max_operations > 0 && state.operations > self.max_operations { return Err(Box::new(EvalAltResult::ErrorTooManyOperations( Position::none(), ))); diff --git a/src/error.rs b/src/error.rs index dd1be4ef..3d4c47c7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,6 +12,8 @@ pub enum LexError { UnexpectedChar(char), /// A string literal is not terminated before a new-line or EOF. UnterminatedString, + /// An identifier is in an invalid format. + StringTooLong(usize), /// An string/character/numeric escape sequence is in an invalid format. MalformedEscapeSequence(String), /// An numeric literal is in an invalid format. @@ -35,6 +37,11 @@ impl fmt::Display for LexError { Self::MalformedChar(s) => write!(f, "Invalid character: '{}'", s), Self::MalformedIdentifier(s) => write!(f, "Variable name is not proper: '{}'", s), Self::UnterminatedString => write!(f, "Open string is not terminated"), + Self::StringTooLong(max) => write!( + f, + "Length of string literal exceeds the maximum limit ({})", + max + ), Self::ImproperKeyword(s) => write!(f, "{}", s), } } @@ -109,12 +116,16 @@ pub enum ParseErrorType { WrongExport, /// Assignment to a copy of a value. AssignmentToCopy, - /// Assignment to an a constant variable. + /// Assignment to an a constant variable. Wrapped value is the constant variable name. AssignmentToConstant(String), /// Expression exceeding the maximum levels of complexity. /// /// Never appears under the `unchecked` feature. ExprTooDeep, + /// Literal exceeding the maximum size. Wrapped values are the data type name and the maximum size. + /// + /// Never appears under the `unchecked` feature. + LiteralTooLarge(String, usize), /// Break statement not inside a loop. LoopBreak, } @@ -149,6 +160,7 @@ impl ParseErrorType { Self::AssignmentToCopy => "Only a copy of the value is change with this assignment", Self::AssignmentToConstant(_) => "Cannot assign to a constant value", Self::ExprTooDeep => "Expression exceeds maximum complexity", + Self::LiteralTooLarge(_, _) => "Literal exceeds maximum limit", Self::LoopBreak => "Break statement should only be used inside a loop" } } @@ -197,6 +209,9 @@ impl fmt::Display for ParseErrorType { Self::AssignmentToConstant(s) if s.is_empty() => write!(f, "{}", self.desc()), Self::AssignmentToConstant(s) => write!(f, "Cannot assign to constant '{}'", s), + Self::LiteralTooLarge(typ, max) => { + write!(f, "{} exceeds the maximum limit ({})", typ, max) + } _ => write!(f, "{}", self.desc()), } } diff --git a/src/parser.rs b/src/parser.rs index 2eed69b1..ec99adaf 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -201,13 +201,27 @@ struct ParseState { stack: Vec<(String, ScopeEntryType)>, /// Maximum levels of expression nesting. max_expr_depth: usize, + /// Maximum length of a string. + pub max_string_size: usize, + /// Maximum length of an array. + pub max_array_size: usize, + /// Maximum number of properties in a map. + pub max_map_size: usize, } impl ParseState { /// Create a new `ParseState`. - pub fn new(max_expr_depth: usize) -> Self { + pub fn new( + max_expr_depth: usize, + max_string_size: usize, + max_array_size: usize, + max_map_size: usize, + ) -> Self { Self { max_expr_depth, + max_string_size, + max_array_size, + max_map_size, ..Default::default() } } @@ -284,7 +298,9 @@ impl ParseSettings { } /// Make sure that the current level of expression nesting is within the maximum limit. pub fn ensure_level_within_max_limit(&self, limit: usize) -> Result<(), ParseError> { - if self.level > limit { + if limit == 0 { + Ok(()) + } else if self.level > limit { Err(PERR::ExprTooDeep.into_err(self.pos)) } else { Ok(()) @@ -610,7 +626,7 @@ impl Expr { Self::Variable(_) => true, - expr => expr.is_constant(), + _ => self.is_constant(), } } @@ -1070,6 +1086,14 @@ fn parse_array_literal( if !match_token(input, Token::RightBracket)? { while !input.peek().unwrap().0.is_eof() { + if state.max_array_size > 0 && arr.len() >= state.max_array_size { + return Err(PERR::LiteralTooLarge( + "Size of array literal".to_string(), + state.max_array_size, + ) + .into_err(input.peek().unwrap().1)); + } + let expr = parse_expr(input, state, settings.level_up())?; arr.push(expr); @@ -1155,8 +1179,15 @@ fn parse_map_literal( } }; - let expr = parse_expr(input, state, settings.level_up())?; + if state.max_map_size > 0 && map.len() >= state.max_map_size { + return Err(PERR::LiteralTooLarge( + "Number of properties in object map literal".to_string(), + state.max_map_size, + ) + .into_err(input.peek().unwrap().1)); + } + let expr = parse_expr(input, state, settings.level_up())?; map.push(((name, pos), expr)); match input.peek().unwrap() { @@ -1239,7 +1270,7 @@ fn parse_primary( Token::True => Expr::True(settings.pos), Token::False => Expr::False(settings.pos), Token::LexError(err) => return Err(PERR::BadInput(err.to_string()).into_err(settings.pos)), - token => { + _ => { return Err( PERR::BadInput(format!("Unexpected '{}'", token.syntax())).into_err(settings.pos) ) @@ -1817,7 +1848,7 @@ fn parse_binary_op( make_dot_expr(current_lhs, rhs, pos)? } - token => return Err(PERR::UnknownOperator(token.into()).into_err(pos)), + op_token => return Err(PERR::UnknownOperator(op_token.into()).into_err(pos)), }; } } @@ -2408,102 +2439,6 @@ fn parse_fn( }) } -/// Parse the global level statements. -fn parse_global_level( - input: &mut TokenStream, - max_expr_depth: usize, - max_function_expr_depth: usize, -) -> Result<(Vec, Vec), ParseError> { - let mut statements = Vec::::new(); - let mut functions = HashMap::::with_hasher(StraightHasherBuilder); - let mut state = ParseState::new(max_expr_depth); - - while !input.peek().unwrap().0.is_eof() { - // Collect all the function definitions - #[cfg(not(feature = "no_function"))] - { - let (access, must_be_fn) = if match_token(input, Token::Private)? { - (FnAccess::Private, true) - } else { - (FnAccess::Public, false) - }; - - match input.peek().unwrap() { - #[cfg(not(feature = "no_function"))] - (Token::Fn, pos) => { - let mut state = ParseState::new(max_function_expr_depth); - let settings = ParseSettings { - allow_if_expr: true, - allow_stmt_expr: true, - is_global: false, - is_breakable: false, - level: 0, - pos: *pos, - }; - let func = parse_fn(input, &mut state, access, settings)?; - - // Qualifiers (none) + function name + number of arguments. - let hash = calc_fn_hash(empty(), &func.name, func.params.len(), empty()); - - functions.insert(hash, func); - continue; - } - (_, pos) if must_be_fn => { - return Err(PERR::MissingToken( - Token::Fn.into(), - format!("following '{}'", Token::Private.syntax()), - ) - .into_err(*pos)) - } - _ => (), - } - } - - // Actual statement - let settings = ParseSettings { - allow_if_expr: true, - allow_stmt_expr: true, - is_global: true, - is_breakable: false, - level: 0, - pos: Position::none(), - }; - let stmt = parse_stmt(input, &mut state, settings)?; - - let need_semicolon = !stmt.is_self_terminated(); - - statements.push(stmt); - - match input.peek().unwrap() { - // EOF - (Token::EOF, _) => break, - // stmt ; - (Token::SemiColon, _) if need_semicolon => { - eat_token(input, Token::SemiColon); - } - // stmt ; - (Token::SemiColon, _) if !need_semicolon => (), - // { stmt } ??? - (_, _) if !need_semicolon => (), - // stmt - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(*pos)) - } - // stmt ??? - (_, pos) => { - // Semicolons are not optional between statements - return Err(PERR::MissingToken( - Token::SemiColon.into(), - "to terminate this statement".into(), - ) - .into_err(*pos)); - } - } - } - - Ok((statements, functions.into_iter().map(|(_, v)| v).collect())) -} - impl Engine { pub(crate) fn parse_global_expr( &self, @@ -2511,7 +2446,12 @@ impl Engine { scope: &Scope, optimization_level: OptimizationLevel, ) -> Result { - let mut state = ParseState::new(self.max_expr_depth); + let mut state = ParseState::new( + self.max_expr_depth, + self.max_string_size, + self.max_array_size, + self.max_map_size, + ); let settings = ParseSettings { allow_if_expr: false, allow_stmt_expr: false, @@ -2540,6 +2480,111 @@ impl Engine { ) } + /// Parse the global level statements. + fn parse_global_level( + &self, + input: &mut TokenStream, + ) -> Result<(Vec, Vec), ParseError> { + let mut statements = Vec::::new(); + let mut functions = HashMap::::with_hasher(StraightHasherBuilder); + let mut state = ParseState::new( + self.max_expr_depth, + self.max_string_size, + self.max_array_size, + self.max_map_size, + ); + + while !input.peek().unwrap().0.is_eof() { + // Collect all the function definitions + #[cfg(not(feature = "no_function"))] + { + let (access, must_be_fn) = if match_token(input, Token::Private)? { + (FnAccess::Private, true) + } else { + (FnAccess::Public, false) + }; + + match input.peek().unwrap() { + #[cfg(not(feature = "no_function"))] + (Token::Fn, pos) => { + let mut state = ParseState::new( + self.max_function_expr_depth, + self.max_string_size, + self.max_array_size, + self.max_map_size, + ); + let settings = ParseSettings { + allow_if_expr: true, + allow_stmt_expr: true, + is_global: false, + is_breakable: false, + level: 0, + pos: *pos, + }; + let func = parse_fn(input, &mut state, access, settings)?; + + // Qualifiers (none) + function name + number of arguments. + let hash = calc_fn_hash(empty(), &func.name, func.params.len(), empty()); + + functions.insert(hash, func); + continue; + } + (_, pos) if must_be_fn => { + return Err(PERR::MissingToken( + Token::Fn.into(), + format!("following '{}'", Token::Private.syntax()), + ) + .into_err(*pos)) + } + _ => (), + } + } + + // Actual statement + let settings = ParseSettings { + allow_if_expr: true, + allow_stmt_expr: true, + is_global: true, + is_breakable: false, + level: 0, + pos: Position::none(), + }; + let stmt = parse_stmt(input, &mut state, settings)?; + + let need_semicolon = !stmt.is_self_terminated(); + + statements.push(stmt); + + match input.peek().unwrap() { + // EOF + (Token::EOF, _) => break, + // stmt ; + (Token::SemiColon, _) if need_semicolon => { + eat_token(input, Token::SemiColon); + } + // stmt ; + (Token::SemiColon, _) if !need_semicolon => (), + // { stmt } ??? + (_, _) if !need_semicolon => (), + // stmt + (Token::LexError(err), pos) => { + return Err(PERR::BadInput(err.to_string()).into_err(*pos)) + } + // stmt ??? + (_, pos) => { + // Semicolons are not optional between statements + return Err(PERR::MissingToken( + Token::SemiColon.into(), + "to terminate this statement".into(), + ) + .into_err(*pos)); + } + } + } + + Ok((statements, functions.into_iter().map(|(_, v)| v).collect())) + } + /// Run the parser on an input stream, returning an AST. pub(crate) fn parse( &self, @@ -2547,8 +2592,7 @@ impl Engine { scope: &Scope, optimization_level: OptimizationLevel, ) -> Result { - let (statements, lib) = - parse_global_level(input, self.max_expr_depth, self.max_function_expr_depth)?; + let (statements, lib) = self.parse_global_level(input)?; Ok( // Optimize AST diff --git a/src/result.rs b/src/result.rs index 36bc18f6..83943df2 100644 --- a/src/result.rs +++ b/src/result.rs @@ -81,6 +81,8 @@ pub enum EvalAltResult { ErrorTooManyModules(Position), /// Call stack over maximum limit. ErrorStackOverflow(Position), + /// Data value over maximum size limit. Wrapped values are the data type, maximum size and current size. + ErrorDataTooLarge(String, usize, usize, Position), /// The script is prematurely terminated. ErrorTerminated(Position), /// Run-time error encountered. Wrapped value is the error message. @@ -139,6 +141,7 @@ impl EvalAltResult { Self::ErrorTooManyOperations(_) => "Too many operations", Self::ErrorTooManyModules(_) => "Too many modules imported", Self::ErrorStackOverflow(_) => "Stack overflow", + Self::ErrorDataTooLarge(_, _, _, _) => "Data size exceeds maximum limit", Self::ErrorTerminated(_) => "Script terminated.", Self::ErrorRuntime(_, _) => "Runtime error", Self::ErrorLoopBreak(true, _) => "Break statement not inside a loop", @@ -228,6 +231,9 @@ impl fmt::Display for EvalAltResult { "String index {} is out of bounds: only {} characters in the string", index, max )?, + Self::ErrorDataTooLarge(typ, max, size, _) => { + write!(f, "{} ({}) exceeds the maximum limit ({})", typ, size, max)? + } } // Do not write any position if None @@ -279,6 +285,7 @@ impl EvalAltResult { | Self::ErrorTooManyOperations(pos) | Self::ErrorTooManyModules(pos) | Self::ErrorStackOverflow(pos) + | Self::ErrorDataTooLarge(_, _, _, pos) | Self::ErrorTerminated(pos) | Self::ErrorRuntime(_, pos) | Self::ErrorLoopBreak(_, pos) @@ -316,6 +323,7 @@ impl EvalAltResult { | Self::ErrorTooManyOperations(pos) | Self::ErrorTooManyModules(pos) | Self::ErrorStackOverflow(pos) + | Self::ErrorDataTooLarge(_, _, _, pos) | Self::ErrorTerminated(pos) | Self::ErrorRuntime(_, pos) | Self::ErrorLoopBreak(_, pos) diff --git a/src/token.rs b/src/token.rs index d6799a6f..614783b4 100644 --- a/src/token.rs +++ b/src/token.rs @@ -429,6 +429,8 @@ impl From for String { /// An iterator on a `Token` stream. pub struct TokenIterator<'a> { + /// Maximum length of a string (0 = unlimited). + max_string_size: usize, /// Can the next token be a unary operator? can_be_unary: bool, /// Current position. @@ -494,6 +496,7 @@ impl<'a> TokenIterator<'a> { pub fn parse_string_literal( &mut self, enclosing_char: char, + max_length: usize, ) -> Result { let mut result = Vec::new(); let mut escape = String::with_capacity(12); @@ -505,6 +508,10 @@ impl<'a> TokenIterator<'a> { self.advance(); + if max_length > 0 && result.len() > max_length { + return Err((LexError::StringTooLong(max_length), self.pos)); + } + match next_char { // \... '\\' if escape.is_empty() => { @@ -592,7 +599,13 @@ impl<'a> TokenIterator<'a> { } } - Ok(result.iter().collect()) + let s = result.iter().collect::(); + + if max_length > 0 && s.len() > max_length { + return Err((LexError::StringTooLong(max_length), self.pos)); + } + + Ok(s) } /// Get the next token. @@ -779,10 +792,12 @@ impl<'a> TokenIterator<'a> { // " - string literal ('"', _) => { - return self.parse_string_literal('"').map_or_else( - |err| Some((Token::LexError(Box::new(err.0)), err.1)), - |out| Some((Token::StringConst(out), pos)), - ); + return self + .parse_string_literal('"', self.max_string_size) + .map_or_else( + |err| Some((Token::LexError(Box::new(err.0)), err.1)), + |out| Some((Token::StringConst(out), pos)), + ); } // ' - character literal @@ -793,19 +808,25 @@ impl<'a> TokenIterator<'a> { )); } ('\'', _) => { - return Some(self.parse_string_literal('\'').map_or_else( - |err| (Token::LexError(Box::new(err.0)), err.1), - |result| { - let mut chars = result.chars(); - let first = chars.next(); + return Some( + self.parse_string_literal('\'', self.max_string_size) + .map_or_else( + |err| (Token::LexError(Box::new(err.0)), err.1), + |result| { + let mut chars = result.chars(); + let first = chars.next(); - if chars.next().is_some() { - (Token::LexError(Box::new(LERR::MalformedChar(result))), pos) - } else { - (Token::CharConstant(first.expect("should be Some")), pos) - } - }, - )); + if chars.next().is_some() { + ( + Token::LexError(Box::new(LERR::MalformedChar(result))), + pos, + ) + } else { + (Token::CharConstant(first.expect("should be Some")), pos) + } + }, + ), + ); } // Braces @@ -1047,8 +1068,9 @@ impl<'a> Iterator for TokenIterator<'a> { } /// Tokenize an input text stream. -pub fn lex<'a>(input: &'a [&'a str]) -> TokenIterator<'a> { +pub fn lex<'a>(input: &'a [&'a str], max_string_size: usize) -> TokenIterator<'a> { TokenIterator { + max_string_size, can_be_unary: true, pos: Position::new(1, 0), streams: input.iter().map(|s| s.chars().peekable()).collect(), diff --git a/tests/data_size.rs b/tests/data_size.rs new file mode 100644 index 00000000..77e660fa --- /dev/null +++ b/tests/data_size.rs @@ -0,0 +1,234 @@ +#![cfg(not(feature = "unchecked"))] +use rhai::{Engine, EvalAltResult, ParseError, ParseErrorType}; + +#[cfg(not(feature = "no_index"))] +use rhai::Array; + +#[cfg(not(feature = "no_object"))] +use rhai::Map; + +#[test] +fn test_max_string_size() -> Result<(), Box> { + let mut engine = Engine::new(); + engine.set_max_string_size(10); + + assert!(matches!( + engine.compile(r#"let x = "hello, world!";"#).expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::BadInput("Length of string literal exceeds the maximum limit (10)".to_string()) + )); + + assert!(matches!( + engine.compile(r#"let x = "朝に紅顔、暮に白骨";"#).expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::BadInput("Length of string literal exceeds the maximum limit (10)".to_string()) + )); + + assert!(matches!( + *engine + .eval::( + r#" + let x = "hello, "; + let y = "world!"; + x + y + "# + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 13, _) + )); + + engine.set_max_string_size(0); + + assert_eq!( + engine.eval::( + r#" + let x = "hello, "; + let y = "world!"; + x + y + "# + )?, + "hello, world!" + ); + + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_index"))] +fn test_max_array_size() -> Result<(), Box> { + let mut engine = Engine::new(); + engine.set_max_array_size(10); + + #[cfg(not(feature = "no_object"))] + engine.set_max_map_size(10); + + assert!(matches!( + engine + .compile("let x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];") + .expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::LiteralTooLarge("Size of array literal".to_string(), 10) + )); + + assert!(matches!( + *engine + .eval::( + r" + let x = [1,2,3,4,5,6]; + let y = [7,8,9,10,11,12]; + x + y + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) + )); + + assert!(matches!( + *engine + .eval::( + r" + let x = [1,2,3]; + [x, x, x, x] + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) + )); + + #[cfg(not(feature = "no_object"))] + assert!(matches!( + *engine + .eval::( + r" + let x = #{a:1, b:2, c:3}; + [x, x, x, x] + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) + )); + + assert!(matches!( + *engine + .eval::( + r" + let x = [1]; + let y = [x, x]; + let z = [y, y]; + [z, z, z] + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) + )); + + engine.set_max_array_size(0); + + assert_eq!( + engine + .eval::( + r" + let x = [1,2,3,4,5,6]; + let y = [7,8,9,10,11,12]; + x + y + " + )? + .len(), + 12 + ); + + assert_eq!( + engine + .eval::( + r" + let x = [1,2,3]; + [x, x, x, x] + " + )? + .len(), + 4 + ); + + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_object"))] +fn test_max_map_size() -> Result<(), Box> { + let mut engine = Engine::new(); + engine.set_max_map_size(10); + + #[cfg(not(feature = "no_index"))] + engine.set_max_array_size(10); + + assert!(matches!( + engine + .compile("let x = #{a:1,b:2,c:3,d:4,e:5,f:6,g:7,h:8,i:9,j:10,k:11,l:12,m:13,n:14,o:15};") + .expect_err("should error"), + ParseError(x, _) if *x == ParseErrorType::LiteralTooLarge("Number of properties in object map literal".to_string(), 10) + )); + + assert!(matches!( + *engine + .eval::( + r" + let x = #{a:1,b:2,c:3,d:4,e:5,f:6}; + let y = #{g:7,h:8,i:9,j:10,k:11,l:12}; + x + y + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) + )); + + assert!(matches!( + *engine + .eval::( + r" + let x = #{a:1,b:2,c:3}; + #{u:x, v:x, w:x, z:x} + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) + )); + + #[cfg(not(feature = "no_index"))] + assert!(matches!( + *engine + .eval::( + r" + let x = [1, 2, 3]; + #{u:x, v:x, w:x, z:x} + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) + )); + + engine.set_max_map_size(0); + + assert_eq!( + engine + .eval::( + r" + let x = #{a:1,b:2,c:3,d:4,e:5,f:6}; + let y = #{g:7,h:8,i:9,j:10,k:11,l:12}; + x + y + " + )? + .len(), + 12 + ); + + assert_eq!( + engine + .eval::( + r" + let x = #{a:1,b:2,c:3}; + #{u:x, v:x, w:x, z:x} + " + )? + .len(), + 4 + ); + + Ok(()) +} diff --git a/tests/modules.rs b/tests/modules.rs index 10c138ad..60d02247 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -130,7 +130,7 @@ fn test_module_resolver() -> Result<(), Box> { EvalAltResult::ErrorInFunctionCall(fn_name, _, _) if fn_name == "foo" )); - engine.set_max_modules(0); + engine.set_max_modules(1000); #[cfg(not(feature = "no_function"))] engine.eval::<()>( diff --git a/tests/operations.rs b/tests/operations.rs index 91df26c9..dae539f0 100644 --- a/tests/operations.rs +++ b/tests/operations.rs @@ -111,3 +111,20 @@ fn test_max_operations_eval() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_max_operations_progress() -> Result<(), Box> { + let mut engine = Engine::new(); + engine.set_max_operations(500); + + engine.on_progress(|&count| count < 100); + + assert!(matches!( + *engine + .eval::<()>("for x in range(0, 500) {}") + .expect_err("should error"), + EvalAltResult::ErrorTerminated(_) + )); + + Ok(()) +} From 0c6a939c666934275bdb3aa18dfb599354f1b79c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 14 Jun 2020 16:56:36 +0800 Subject: [PATCH 37/53] Better convert LexError to ParseError. --- README.md | 2 +- src/error.rs | 24 ++++++++++++++--- src/parser.rs | 65 ++++++++++++++-------------------------------- tests/data_size.rs | 4 +-- 4 files changed, 44 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index c0931fa6..7bea89bb 100644 --- a/README.md +++ b/README.md @@ -2480,7 +2480,7 @@ engine.set_max_string_size(500); // allow strings only up to 500 byte engine.set_max_string_size(0); // allow unlimited string length ``` -A script attempting to create a string literal longer than the maximum will terminate with a parse error. +A script attempting to create a string literal longer than the maximum length will terminate with a parse error. Any script operation that produces a string longer than the maximum also terminates the script with an error result. This check can be disabled via the [`unchecked`] feature for higher performance (but higher risks as well). diff --git a/src/error.rs b/src/error.rs index 3d4c47c7..ce8615ed 100644 --- a/src/error.rs +++ b/src/error.rs @@ -22,8 +22,8 @@ pub enum LexError { MalformedChar(String), /// An identifier is in an invalid format. MalformedIdentifier(String), - /// Bad keyword encountered when tokenizing the script text. - ImproperKeyword(String), + /// Bad symbol encountered when tokenizing the script text. + ImproperSymbol(String), } impl Error for LexError {} @@ -42,11 +42,18 @@ impl fmt::Display for LexError { "Length of string literal exceeds the maximum limit ({})", max ), - Self::ImproperKeyword(s) => write!(f, "{}", s), + Self::ImproperSymbol(s) => write!(f, "{}", s), } } } +impl LexError { + /// Convert a `LexError` into a `ParseError`. + pub fn into_err(&self, pos: Position) -> ParseError { + ParseError(Box::new(self.into()), pos) + } +} + /// Type of error encountered when parsing a script. /// /// Some errors never appear when certain features are turned on. @@ -217,6 +224,17 @@ impl fmt::Display for ParseErrorType { } } +impl From<&LexError> for ParseErrorType { + fn from(err: &LexError) -> Self { + match err { + LexError::StringTooLong(max) => { + Self::LiteralTooLarge("Length of string literal".to_string(), *max) + } + _ => Self::BadInput(err.to_string()), + } + } +} + /// Error when parsing a script. #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub struct ParseError(pub Box, pub Position); diff --git a/src/parser.rs b/src/parser.rs index ec99adaf..acbb59e5 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -767,7 +767,7 @@ fn parse_paren_expr( // ( xxx ) (Token::RightParen, _) => Ok(expr), // ( - (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), // ( xxx ??? (_, pos) => Err(PERR::MissingToken( Token::RightParen.into(), @@ -800,7 +800,7 @@ fn parse_call_expr( .into_err(settings.pos)) } // id - Token::LexError(err) => return Err(PERR::BadInput(err.to_string()).into_err(settings.pos)), + Token::LexError(err) => return Err(err.into_err(settings.pos)), // id() Token::RightParen => { eat_token(input, Token::RightParen); @@ -880,9 +880,7 @@ fn parse_call_expr( .into_err(*pos)) } // id(...args - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(*pos)) - } + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), // id(...args ??? (_, pos) => { return Err(PERR::MissingToken( @@ -1065,7 +1063,7 @@ fn parse_index_chain( } } } - (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(*pos)), + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), (_, pos) => Err(PERR::MissingToken( Token::RightBracket.into(), "for a matching [ in this index expression".into(), @@ -1110,9 +1108,7 @@ fn parse_array_literal( ) .into_err(*pos)) } - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(*pos)) - } + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), (_, pos) => { return Err(PERR::MissingToken( Token::Comma.into(), @@ -1144,9 +1140,7 @@ fn parse_map_literal( let (name, pos) = match input.next().unwrap() { (Token::Identifier(s), pos) => (s, pos), (Token::StringConst(s), pos) => (s, pos), - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(pos)) - } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) if map.is_empty() => { return Err( PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) @@ -1164,9 +1158,7 @@ fn parse_map_literal( match input.next().unwrap() { (Token::Colon, _) => (), - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(pos)) - } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => { return Err(PERR::MissingToken( Token::Colon.into(), @@ -1205,9 +1197,7 @@ fn parse_map_literal( ) .into_err(*pos)) } - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(*pos)) - } + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), (_, pos) => { return Err( PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) @@ -1269,7 +1259,7 @@ fn parse_primary( Token::MapStart => parse_map_literal(input, state, settings.level_up())?, Token::True => Expr::True(settings.pos), Token::False => Expr::False(settings.pos), - Token::LexError(err) => return Err(PERR::BadInput(err.to_string()).into_err(settings.pos)), + Token::LexError(err) => return Err(err.into_err(settings.pos)), _ => { return Err( PERR::BadInput(format!("Unexpected '{}'", token.syntax())).into_err(settings.pos) @@ -1380,12 +1370,7 @@ fn parse_unary( None } }) - .ok_or_else(|| { - PERR::BadInput( - LexError::MalformedNumber(format!("-{}", x.0)).to_string(), - ) - .into_err(pos) - }) + .ok_or_else(|| LexError::MalformedNumber(format!("-{}", x.0)).into_err(pos)) } // Negative float @@ -1990,7 +1975,7 @@ fn parse_for( // Variable name (Token::Identifier(s), _) => s, // Bad identifier - (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), // EOF (Token::EOF, pos) => return Err(PERR::VariableExpected.into_err(pos)), // Not a variable name @@ -2000,7 +1985,7 @@ fn parse_for( // for name in ... match input.next().unwrap() { (Token::In, _) => (), - (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => { return Err( PERR::MissingToken(Token::In.into(), "after the iteration variable".into()) @@ -2038,7 +2023,7 @@ fn parse_let( // let name ... let (name, pos) = match input.next().unwrap() { (Token::Identifier(s), pos) => (s, pos), - (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; @@ -2109,7 +2094,7 @@ fn parse_import( // import expr as name ... let (name, _) = match input.next().unwrap() { (Token::Identifier(s), pos) => (s, pos), - (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; @@ -2132,9 +2117,7 @@ fn parse_export( loop { let (id, id_pos) = match input.next().unwrap() { (Token::Identifier(s), pos) => (s.clone(), pos), - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(pos)) - } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; @@ -2189,7 +2172,7 @@ fn parse_block( // Must start with { settings.pos = match input.next().unwrap() { (Token::LeftBrace, pos) => pos, - (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(pos)), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => { return Err(PERR::MissingToken( Token::LeftBrace.into(), @@ -2229,9 +2212,7 @@ fn parse_block( // { ... { stmt } ??? (_, _) if !need_semicolon => (), // { ... stmt - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(*pos)) - } + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), // { ... stmt ??? (_, pos) => { // Semicolons are not optional between statements @@ -2380,9 +2361,7 @@ fn parse_fn( state.push((s.clone(), ScopeEntryType::Normal)); params.push((s, pos)) } - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(pos)) - } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => { return Err(PERR::MissingToken(Token::RightParen.into(), end_err).into_err(pos)) } @@ -2394,9 +2373,7 @@ fn parse_fn( (Token::Identifier(_), pos) => { return Err(PERR::MissingToken(Token::Comma.into(), sep_err).into_err(pos)) } - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(pos)) - } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => { return Err(PERR::MissingToken(Token::Comma.into(), sep_err).into_err(pos)) } @@ -2567,9 +2544,7 @@ impl Engine { // { stmt } ??? (_, _) if !need_semicolon => (), // stmt - (Token::LexError(err), pos) => { - return Err(PERR::BadInput(err.to_string()).into_err(*pos)) - } + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), // stmt ??? (_, pos) => { // Semicolons are not optional between statements diff --git a/tests/data_size.rs b/tests/data_size.rs index 77e660fa..7884971f 100644 --- a/tests/data_size.rs +++ b/tests/data_size.rs @@ -14,12 +14,12 @@ fn test_max_string_size() -> Result<(), Box> { assert!(matches!( engine.compile(r#"let x = "hello, world!";"#).expect_err("should error"), - ParseError(x, _) if *x == ParseErrorType::BadInput("Length of string literal exceeds the maximum limit (10)".to_string()) + ParseError(x, _) if *x == ParseErrorType::LiteralTooLarge("Length of string literal".to_string(), 10) )); assert!(matches!( engine.compile(r#"let x = "朝に紅顔、暮に白骨";"#).expect_err("should error"), - ParseError(x, _) if *x == ParseErrorType::BadInput("Length of string literal exceeds the maximum limit (10)".to_string()) + ParseError(x, _) if *x == ParseErrorType::LiteralTooLarge("Length of string literal".to_string(), 10) )); assert!(matches!( From f26c12b8ea9e95ad023f45f3fae56297d865e6c7 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 14 Jun 2020 19:13:11 +0800 Subject: [PATCH 38/53] Better error messages for unrecognized tokens. --- src/token.rs | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/token.rs b/src/token.rs index 614783b4..0bb0cbe3 100644 --- a/src/token.rs +++ b/src/token.rs @@ -862,6 +862,14 @@ impl<'a> TokenIterator<'a> { self.eat_next(); return Some((Token::MinusAssign, pos)); } + ('-', '>') => { + return Some(( + Token::LexError(Box::new(LERR::ImproperSymbol( + "'->' is not a valid symbol. This is not C or C++!".to_string(), + ))), + pos, + )) + } ('-', _) if self.can_be_unary => return Some((Token::UnaryMinus, pos)), ('-', _) => return Some((Token::Minus, pos)), @@ -931,7 +939,7 @@ impl<'a> TokenIterator<'a> { // Warn against `===` if self.peek_next() == Some('=') { return Some(( - Token::LexError(Box::new(LERR::ImproperKeyword( + Token::LexError(Box::new(LERR::ImproperSymbol( "'===' is not a valid operator. This is not JavaScript! Should it be '=='?" .to_string(), ))), @@ -941,18 +949,44 @@ impl<'a> TokenIterator<'a> { return Some((Token::EqualsTo, pos)); } + ('=', '>') => { + return Some(( + Token::LexError(Box::new(LERR::ImproperSymbol( + "'=>' is not a valid symbol. This is not Rust! Should it be '>='?" + .to_string(), + ))), + pos, + )) + } ('=', _) => return Some((Token::Equals, pos)), (':', ':') => { self.eat_next(); return Some((Token::DoubleColon, pos)); } + (':', '=') => { + return Some(( + Token::LexError(Box::new(LERR::ImproperSymbol( + "':=' is not a valid assignment operator. This is not Pascal! Should it be simply '='?" + .to_string(), + ))), + pos, + )) + } (':', _) => return Some((Token::Colon, pos)), ('<', '=') => { self.eat_next(); return Some((Token::LessThanEqualsTo, pos)); } + ('<', '-') => { + return Some(( + Token::LexError(Box::new(LERR::ImproperSymbol( + "'<-' is not a valid symbol. Should it be '<='?".to_string(), + ))), + pos, + )) + } ('<', '<') => { self.eat_next(); @@ -993,7 +1027,7 @@ impl<'a> TokenIterator<'a> { // Warn against `!==` if self.peek_next() == Some('=') { return Some(( - Token::LexError(Box::new(LERR::ImproperKeyword( + Token::LexError(Box::new(LERR::ImproperSymbol( "'!==' is not a valid operator. This is not JavaScript! Should it be '!='?" .to_string(), ))), From 31d2fa410bc943150b0e1ecf28893563931fa8a5 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 14 Jun 2020 22:44:59 +0800 Subject: [PATCH 39/53] Streamline code. --- README.md | 2 +- src/engine.rs | 48 +++++++++++++++++++++++++----------------------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 7bea89bb..642b660d 100644 --- a/README.md +++ b/README.md @@ -2207,7 +2207,7 @@ let a = new_ts(); // constructor function a.field = 500; // property setter a.update(); // method call, 'a' can be modified -update(a); // <- this de-sugars to 'a.update()' this if 'a' is a simple variable +update(a); // <- this de-sugars to 'a.update()' thus if 'a' is a simple variable // unlike scripted functions, 'a' can be modified and is not a copy let array = [ a ]; diff --git a/src/engine.rs b/src/engine.rs index 83878c3c..7ce521ea 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -63,9 +63,9 @@ pub const MAX_FUNCTION_EXPR_DEPTH: usize = 32; #[cfg(feature = "unchecked")] pub const MAX_CALL_STACK_DEPTH: usize = usize::MAX; #[cfg(feature = "unchecked")] -pub const MAX_EXPR_DEPTH: usize = usize::MAX; +pub const MAX_EXPR_DEPTH: usize = 0; #[cfg(feature = "unchecked")] -pub const MAX_FUNCTION_EXPR_DEPTH: usize = usize::MAX; +pub const MAX_FUNCTION_EXPR_DEPTH: usize = 0; pub const KEYWORD_PRINT: &str = "print"; pub const KEYWORD_DEBUG: &str = "debug"; @@ -1761,11 +1761,7 @@ impl Engine { _ => unreachable!(), }; - if let Ok(val) = &result { - self.check_data_size(val)?; - } - - result + self.check_data_size(result) } /// Evaluate a statement @@ -2034,20 +2030,20 @@ impl Engine { } }; - if let Ok(val) = &result { - self.check_data_size(val)?; - } - - result + self.check_data_size(result) } - /// Check a `Dynamic` value to ensure that its size is within allowable limit. - fn check_data_size(&self, value: &Dynamic) -> Result<(), Box> { + /// Check a result to ensure that the data size is within allowable limit. + fn check_data_size( + &self, + result: Result>, + ) -> Result> { #[cfg(feature = "unchecked")] - return Ok(()); + return result; + // If no data size limits, just return if self.max_string_size + self.max_array_size + self.max_map_size == 0 { - return Ok(()); + return result; } // Recursively calculate the size of a value (especially `Array` and `Map`) @@ -2090,16 +2086,22 @@ impl Engine { } } - match value { - Dynamic(Union::Str(_)) if self.max_string_size > 0 => (), + match result { + // Simply return all errors + Err(_) => return result, + // String with limit + Ok(Dynamic(Union::Str(_))) if self.max_string_size > 0 => (), + // Array with limit #[cfg(not(feature = "no_index"))] - Dynamic(Union::Array(_)) if self.max_array_size > 0 => (), + Ok(Dynamic(Union::Array(_))) if self.max_array_size > 0 => (), + // Map with limit #[cfg(not(feature = "no_object"))] - Dynamic(Union::Map(_)) if self.max_map_size > 0 => (), - _ => return Ok(()), + Ok(Dynamic(Union::Map(_))) if self.max_map_size > 0 => (), + // Everything else is simply returned + Ok(_) => return result, }; - let (arr, map, s) = calc_size(value); + let (arr, map, s) = calc_size(result.as_ref().unwrap()); if s > self.max_string_size { Err(Box::new(EvalAltResult::ErrorDataTooLarge( @@ -2123,7 +2125,7 @@ impl Engine { Position::none(), ))) } else { - Ok(()) + result } } From a417bdd8e3ecf4fdfc74d224af2b8c3a36972710 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 15 Jun 2020 21:49:02 +0800 Subject: [PATCH 40/53] Support registering functions with a reference to the scripting engine. --- README.md | 6 +---- src/api.rs | 10 +++----- src/engine.rs | 10 ++++---- src/fn_native.rs | 6 +++-- src/fn_register.rs | 3 +-- src/module.rs | 48 +++++++++++++++++++++++++++---------- src/packages/array_basic.rs | 41 +++++++++++++++++++++++++------ src/packages/string_more.rs | 43 ++++++++++++++++++++++++++++----- tests/data_size.rs | 25 +++++++++++++++++++ tests/modules.rs | 5 +++- 10 files changed, 151 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 642b660d..78fbae94 100644 --- a/README.md +++ b/README.md @@ -2488,9 +2488,7 @@ This check can be disabled via the [`unchecked`] feature for higher performance Be conservative when setting a maximum limit and always consider the fact that a registered function may grow a string's length without Rhai noticing until the very end. For instance, the built-in '`+`' operator for strings concatenates two strings together to form one longer string; if both strings are _slightly_ below the maximum -length limit, the resultant string may be almost _twice_ the maximum length. The '`pad`' function grows a string -to a specified length which may be longer than the allowed maximum -(to trap this risk, register a custom '`pad`' function that checks the arguments). +length limit, the resultant string may be almost _twice_ the maximum length. ### Maximum size of arrays @@ -2514,8 +2512,6 @@ Be conservative when setting a maximum limit and always consider the fact that a an array's size without Rhai noticing until the very end. For instance, the built-in '`+`' operator for arrays concatenates two arrays together to form one larger array; if both arrays are _slightly_ below the maximum size limit, the resultant array may be almost _twice_ the maximum size. -The '`pad`' function grows an array to a specified size which may be larger than the allowed maximum -(to trap this risk, register a custom '`pad`' function that checks the arguments). As a malicious script may create a deeply-nested array which consumes huge amounts of memory while each individual array still stays under the maximum size limit, Rhai also recursively adds up the sizes of all strings, arrays diff --git a/src/api.rs b/src/api.rs index 56adc08c..6a6dbf5c 100644 --- a/src/api.rs +++ b/src/api.rs @@ -751,7 +751,6 @@ impl Engine { ) -> Result { let scripts = [script]; let stream = lex(&scripts, self.max_string_size); - { let mut peekable = stream.peekable(); self.parse_global_expr(&mut peekable, scope, self.optimization_level) @@ -906,11 +905,8 @@ impl Engine { let scripts = [script]; let stream = lex(&scripts, self.max_string_size); - let ast = self.parse_global_expr( - &mut stream.peekable(), - scope, - OptimizationLevel::None, // No need to optimize a lone expression - )?; + // No need to optimize a lone expression + let ast = self.parse_global_expr(&mut stream.peekable(), scope, OptimizationLevel::None)?; self.eval_ast_with_scope(scope, &ast) } @@ -983,6 +979,7 @@ impl Engine { }); } + /// Evaluate an `AST` with own scope. pub(crate) fn eval_ast_with_scope_raw( &self, scope: &mut Scope, @@ -1035,7 +1032,6 @@ impl Engine { ) -> Result<(), Box> { let scripts = [script]; let stream = lex(&scripts, self.max_string_size); - let ast = self.parse(&mut stream.peekable(), scope, self.optimization_level)?; self.consume_ast_with_scope(scope, &ast) } diff --git a/src/engine.rs b/src/engine.rs index 7ce521ea..8ca14356 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -645,7 +645,7 @@ impl Engine { return Ok((result, false)); } else { // Run external function - let result = func.get_native_fn()(args)?; + let result = func.get_native_fn()(self, args)?; // Restore the original reference restore_first_arg(old_this_ptr, args); @@ -1474,7 +1474,7 @@ impl Engine { .or_else(|| self.packages.get_fn(hash_fn)) { // Overriding exact implementation - func(&mut [lhs_ptr, &mut rhs_val])?; + func(self, &mut [lhs_ptr, &mut rhs_val])?; } else if run_builtin_op_assignment(op, lhs_ptr, &rhs_val)?.is_none() { // Not built in, map to `var = var op rhs` let op = &op[..op.len() - 1]; // extract operator without = @@ -1705,7 +1705,9 @@ impl Engine { self.call_script_fn(&mut scope, state, lib, name, fn_def, args, level) .map_err(|err| EvalAltResult::new_position(err, *pos)) } - Ok(f) => f.get_native_fn()(args.as_mut()).map_err(|err| err.new_position(*pos)), + Ok(f) => { + f.get_native_fn()(self, args.as_mut()).map_err(|err| err.new_position(*pos)) + } Err(err) if def_val.is_some() && matches!(*err, EvalAltResult::ErrorFunctionNotFound(_, _)) => @@ -2112,7 +2114,7 @@ impl Engine { ))) } else if arr > self.max_array_size { Err(Box::new(EvalAltResult::ErrorDataTooLarge( - "Length of array".to_string(), + "Size of array".to_string(), self.max_array_size, arr, Position::none(), diff --git a/src/fn_native.rs b/src/fn_native.rs index fc478512..b7805ff5 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -1,4 +1,5 @@ use crate::any::Dynamic; +use crate::engine::Engine; use crate::parser::ScriptFnDef; use crate::result::EvalAltResult; @@ -51,9 +52,10 @@ pub fn shared_take(value: Shared) -> T { pub type FnCallArgs<'a> = [&'a mut Dynamic]; #[cfg(not(feature = "sync"))] -pub type FnAny = dyn Fn(&mut FnCallArgs) -> Result>; +pub type FnAny = dyn Fn(&Engine, &mut FnCallArgs) -> Result>; #[cfg(feature = "sync")] -pub type FnAny = dyn Fn(&mut FnCallArgs) -> Result> + Send + Sync; +pub type FnAny = + dyn Fn(&Engine, &mut FnCallArgs) -> Result> + Send + Sync; pub type IteratorFn = fn(Dynamic) -> Box>; diff --git a/src/fn_register.rs b/src/fn_register.rs index 86e7ada9..1b07cbe8 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -1,5 +1,4 @@ //! Module which defines the function registration mechanism. - #![allow(non_snake_case)] use crate::any::{Dynamic, Variant}; @@ -120,7 +119,7 @@ macro_rules! make_func { // ^ function parameter generic type name (A, B, C etc.) // ^ dereferencing function - Box::new(move |args: &mut FnCallArgs| { + Box::new(move |_: &Engine, args: &mut FnCallArgs| { // The arguments are assumed to be of the correct number and types! #[allow(unused_variables, unused_mut)] diff --git a/src/module.rs b/src/module.rs index 91fa3388..d1206b5d 100644 --- a/src/module.rs +++ b/src/module.rs @@ -305,6 +305,29 @@ impl Module { hash_fn } + /// Set a Rust function taking a reference to the scripting `Engine`, plus a list of + /// mutable `Dynamic` references into the module, returning a hash key. + /// A list of `TypeId`'s is taken as the argument types. + /// + /// Use this to register a built-in function which must reference settings on the scripting + /// `Engine` (e.g. to prevent growing an array beyond the allowed maximum size). + /// + /// If there is a similar existing Rust function, it is replaced. + pub(crate) fn set_fn_var_args( + &mut self, + name: impl Into, + args: &[TypeId], + func: impl Fn(&Engine, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, + ) -> u64 { + let f = move |engine: &Engine, args: &mut FnCallArgs| func(engine, args).map(Dynamic::from); + self.set_fn( + name, + Public, + args, + CallableFunction::from_method(Box::new(f)), + ) + } + /// Set a Rust function taking no parameters into the module, returning a hash key. /// /// If there is a similar existing Rust function, it is replaced. @@ -323,7 +346,7 @@ impl Module { name: impl Into, func: impl Fn() -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |_: &mut FnCallArgs| func().map(Dynamic::from); + let f = move |_: &Engine, _: &mut FnCallArgs| func().map(Dynamic::from); let args = []; self.set_fn( name, @@ -351,8 +374,9 @@ impl Module { name: impl Into, func: impl Fn(A) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = - move |args: &mut FnCallArgs| func(mem::take(args[0]).cast::()).map(Dynamic::from); + let f = move |_: &Engine, args: &mut FnCallArgs| { + func(mem::take(args[0]).cast::()).map(Dynamic::from) + }; let args = [TypeId::of::()]; self.set_fn( name, @@ -380,7 +404,7 @@ impl Module { name: impl Into, func: impl Fn(&mut A) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |args: &mut FnCallArgs| { + let f = move |_: &Engine, args: &mut FnCallArgs| { func(args[0].downcast_mut::().unwrap()).map(Dynamic::from) }; let args = [TypeId::of::()]; @@ -434,7 +458,7 @@ impl Module { name: impl Into, func: impl Fn(A, B) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |args: &mut FnCallArgs| { + let f = move |_: &Engine, args: &mut FnCallArgs| { let a = mem::take(args[0]).cast::(); let b = mem::take(args[1]).cast::(); @@ -470,7 +494,7 @@ impl Module { name: impl Into, func: impl Fn(&mut A, B) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |args: &mut FnCallArgs| { + let f = move |_: &Engine, args: &mut FnCallArgs| { let b = mem::take(args[1]).cast::(); let a = args[0].downcast_mut::().unwrap(); @@ -561,7 +585,7 @@ impl Module { name: impl Into, func: impl Fn(A, B, C) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |args: &mut FnCallArgs| { + let f = move |_: &Engine, args: &mut FnCallArgs| { let a = mem::take(args[0]).cast::(); let b = mem::take(args[1]).cast::(); let c = mem::take(args[2]).cast::(); @@ -603,7 +627,7 @@ impl Module { name: impl Into, func: impl Fn(&mut A, B, C) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |args: &mut FnCallArgs| { + let f = move |_: &Engine, args: &mut FnCallArgs| { let b = mem::take(args[1]).cast::(); let c = mem::take(args[2]).cast::(); let a = args[0].downcast_mut::().unwrap(); @@ -640,7 +664,7 @@ impl Module { &mut self, func: impl Fn(&mut A, B, A) -> FuncReturn<()> + SendSync + 'static, ) -> u64 { - let f = move |args: &mut FnCallArgs| { + let f = move |_: &Engine, args: &mut FnCallArgs| { let b = mem::take(args[1]).cast::(); let c = mem::take(args[2]).cast::(); let a = args[0].downcast_mut::().unwrap(); @@ -682,7 +706,7 @@ impl Module { name: impl Into, func: impl Fn(A, B, C, D) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |args: &mut FnCallArgs| { + let f = move |_: &Engine, args: &mut FnCallArgs| { let a = mem::take(args[0]).cast::(); let b = mem::take(args[1]).cast::(); let c = mem::take(args[2]).cast::(); @@ -731,7 +755,7 @@ impl Module { name: impl Into, func: impl Fn(&mut A, B, C, D) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |args: &mut FnCallArgs| { + let f = move |_: &Engine, args: &mut FnCallArgs| { let b = mem::take(args[1]).cast::(); let c = mem::take(args[2]).cast::(); let d = mem::take(args[3]).cast::(); @@ -1019,7 +1043,7 @@ pub trait ModuleResolver: SendSync { /// Resolve a module based on a path string. fn resolve( &self, - engine: &Engine, + _: &Engine, scope: Scope, path: &str, pos: Position, diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index 19206e36..c8615865 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -2,9 +2,11 @@ use crate::any::{Dynamic, Variant}; use crate::def_package; -use crate::engine::Array; +use crate::engine::{Array, Engine}; use crate::module::FuncReturn; use crate::parser::{ImmutableString, INT}; +use crate::result::EvalAltResult; +use crate::token::Position; use crate::stdlib::{any::TypeId, boxed::Box}; @@ -23,13 +25,28 @@ fn ins(list: &mut Array, position: INT, item: T) -> FuncRetu } Ok(()) } -fn pad(list: &mut Array, len: INT, item: T) -> FuncReturn<()> { - if len >= 0 { +fn pad(engine: &Engine, args: &mut [&mut Dynamic]) -> FuncReturn<()> { + let len = *args[1].downcast_ref::().unwrap(); + + // Check if array will be over max size limit + if engine.max_array_size > 0 && len > 0 && (len as usize) > engine.max_array_size { + Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Size of array".to_string(), + engine.max_array_size, + len as usize, + Position::none(), + ))) + } else if len >= 0 { + let item = args[2].downcast_ref::().unwrap().clone(); + let list = args[0].downcast_mut::().unwrap(); + while list.len() < len as usize { push(list, item.clone())?; } + Ok(()) + } else { + Ok(()) } - Ok(()) } macro_rules! reg_op { @@ -42,11 +59,21 @@ macro_rules! reg_tri { $( $lib.set_fn_3_mut($op, $func::<$par>); )* }; } +macro_rules! reg_pad { + ($lib:expr, $op:expr, $func:ident, $($par:ty),*) => { + $({ + $lib.set_fn_var_args($op, + &[TypeId::of::(), TypeId::of::(), TypeId::of::<$par>()], + $func::<$par> + ); + })* + }; +} #[cfg(not(feature = "no_index"))] def_package!(crate:BasicArrayPackage:"Basic array utilities.", lib, { reg_op!(lib, "push", push, INT, bool, char, ImmutableString, Array, ()); - reg_tri!(lib, "pad", pad, INT, bool, char, ImmutableString, Array, ()); + reg_pad!(lib, "pad", pad, INT, bool, char, ImmutableString, Array, ()); reg_tri!(lib, "insert", ins, INT, bool, char, ImmutableString, Array, ()); lib.set_fn_2_mut("append", |x: &mut Array, y: Array| { @@ -69,14 +96,14 @@ def_package!(crate:BasicArrayPackage:"Basic array utilities.", lib, { #[cfg(not(feature = "only_i64"))] { reg_op!(lib, "push", push, i8, u8, i16, u16, i32, i64, u32, u64, i128, u128); - reg_tri!(lib, "pad", pad, i8, u8, i16, u16, i32, u32, i64, u64, i128, u128); + reg_pad!(lib, "pad", pad, i8, u8, i16, u16, i32, u32, i64, u64, i128, u128); reg_tri!(lib, "insert", ins, i8, u8, i16, u16, i32, i64, u32, u64, i128, u128); } #[cfg(not(feature = "no_float"))] { reg_op!(lib, "push", push, f32, f64); - reg_tri!(lib, "pad", pad, f32, f64); + reg_pad!(lib, "pad", pad, f32, f64); reg_tri!(lib, "insert", ins, f32, f64); } diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index e86f5fb1..6b5928c1 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -1,12 +1,17 @@ +use crate::any::Dynamic; use crate::def_package; +use crate::engine::Engine; use crate::module::FuncReturn; use crate::parser::{ImmutableString, INT}; +use crate::result::EvalAltResult; +use crate::token::Position; use crate::utils::StaticVec; #[cfg(not(feature = "no_index"))] use crate::engine::Array; use crate::stdlib::{ + any::TypeId, fmt::Display, format, string::{String, ToString}, @@ -210,14 +215,40 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str Ok(()) }, ); - lib.set_fn_3_mut( + lib.set_fn_var_args( "pad", - |s: &mut ImmutableString, len: INT, ch: char| { - let copy = s.make_mut(); - for _ in 0..copy.chars().count() - len as usize { - copy.push(ch); + &[TypeId::of::(), TypeId::of::(), TypeId::of::()], + |engine: &Engine, args: &mut [&mut Dynamic]| { + let len = *args[1].downcast_ref::< INT>().unwrap(); + + // Check if string will be over max size limit + if engine.max_string_size > 0 && len > 0 && (len as usize) > engine.max_string_size { + Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Length of string".to_string(), + engine.max_string_size, + len as usize, + Position::none(), + ))) + } else { + let ch = *args[2].downcast_ref::< char>().unwrap(); + let s = args[0].downcast_mut::().unwrap(); + + let copy = s.make_mut(); + for _ in 0..copy.chars().count() - len as usize { + copy.push(ch); + } + + if engine.max_string_size > 0 && copy.len() > engine.max_string_size { + Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Length of string".to_string(), + engine.max_string_size, + copy.len(), + Position::none(), + ))) + } else { + Ok(()) + } } - Ok(()) }, ); lib.set_fn_3_mut( diff --git a/tests/data_size.rs b/tests/data_size.rs index 7884971f..e073d443 100644 --- a/tests/data_size.rs +++ b/tests/data_size.rs @@ -35,6 +35,19 @@ fn test_max_string_size() -> Result<(), Box> { EvalAltResult::ErrorDataTooLarge(_, 10, 13, _) )); + assert!(matches!( + *engine + .eval::( + r#" + let x = "hello"; + x.pad(100, '!'); + x + "# + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 100, _) + )); + engine.set_max_string_size(0); assert_eq!( @@ -79,6 +92,18 @@ fn test_max_array_size() -> Result<(), Box> { .expect_err("should error"), EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) )); + assert!(matches!( + *engine + .eval::( + r" + let x = [1,2,3,4,5,6]; + x.pad(100, 42); + x + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, 10, 100, _) + )); assert!(matches!( *engine diff --git a/tests/modules.rs b/tests/modules.rs index 60d02247..771aa490 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -1,7 +1,9 @@ #![cfg(not(feature = "no_module"))] use rhai::{ - module_resolvers, Engine, EvalAltResult, Module, ParseError, ParseErrorType, Scope, INT, + module_resolvers, Dynamic, Engine, EvalAltResult, Module, ParseError, ParseErrorType, Scope, + INT, }; +use std::any::TypeId; #[test] fn test_module() { @@ -20,6 +22,7 @@ fn test_module_sub_module() -> Result<(), Box> { let mut sub_module2 = Module::new(); sub_module2.set_var("answer", 41 as INT); + let hash_inc = sub_module2.set_fn_1("inc", |x: INT| Ok(x + 1)); sub_module.set_sub_module("universe", sub_module2); From fcb5059570f91730530b42bd7f4d44832fb25774 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 15 Jun 2020 22:04:14 +0800 Subject: [PATCH 41/53] Do not check for data oversize when unchecked. --- src/packages/array_basic.rs | 25 +++++++++++---------- src/packages/string_more.rs | 43 ++++++++++++++++++++----------------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index c8615865..c6904e90 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -29,24 +29,27 @@ fn pad(engine: &Engine, args: &mut [&mut Dynamic]) -> FuncRe let len = *args[1].downcast_ref::().unwrap(); // Check if array will be over max size limit - if engine.max_array_size > 0 && len > 0 && (len as usize) > engine.max_array_size { - Err(Box::new(EvalAltResult::ErrorDataTooLarge( - "Size of array".to_string(), - engine.max_array_size, - len as usize, - Position::none(), - ))) - } else if len >= 0 { + #[cfg(not(feature = "unchecked"))] + { + if engine.max_array_size > 0 && len > 0 && (len as usize) > engine.max_array_size { + return Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Size of array".to_string(), + engine.max_array_size, + len as usize, + Position::none(), + ))); + } + } + + if len >= 0 { let item = args[2].downcast_ref::().unwrap().clone(); let list = args[0].downcast_mut::().unwrap(); while list.len() < len as usize { push(list, item.clone())?; } - Ok(()) - } else { - Ok(()) } + Ok(()) } macro_rules! reg_op { diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 6b5928c1..804af8ef 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -222,32 +222,35 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str let len = *args[1].downcast_ref::< INT>().unwrap(); // Check if string will be over max size limit - if engine.max_string_size > 0 && len > 0 && (len as usize) > engine.max_string_size { + #[cfg(not(feature = "unchecked"))] + { + if engine.max_string_size > 0 && len > 0 && (len as usize) > engine.max_string_size { + return Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Length of string".to_string(), + engine.max_string_size, + len as usize, + Position::none(), + ))); + } + } + + let ch = *args[2].downcast_ref::< char>().unwrap(); + let s = args[0].downcast_mut::().unwrap(); + + let copy = s.make_mut(); + for _ in 0..copy.chars().count() - len as usize { + copy.push(ch); + } + + if engine.max_string_size > 0 && copy.len() > engine.max_string_size { Err(Box::new(EvalAltResult::ErrorDataTooLarge( "Length of string".to_string(), engine.max_string_size, - len as usize, + copy.len(), Position::none(), ))) } else { - let ch = *args[2].downcast_ref::< char>().unwrap(); - let s = args[0].downcast_mut::().unwrap(); - - let copy = s.make_mut(); - for _ in 0..copy.chars().count() - len as usize { - copy.push(ch); - } - - if engine.max_string_size > 0 && copy.len() > engine.max_string_size { - Err(Box::new(EvalAltResult::ErrorDataTooLarge( - "Length of string".to_string(), - engine.max_string_size, - copy.len(), - Position::none(), - ))) - } else { - Ok(()) - } + Ok(()) } }, ); From 4603f8026f28809985af20ea8912b3bb4b497830 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 15 Jun 2020 23:20:50 +0800 Subject: [PATCH 42/53] Add getters for Engine setting fields. --- src/engine.rs | 92 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 85 insertions(+), 7 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 79924129..e1beb635 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -230,6 +230,9 @@ pub fn get_script_function_by_signature<'a>( /// /// Currently, `Engine` is neither `Send` nor `Sync`. Use the `sync` feature to make it `Send + Sync`. pub struct Engine { + /// A unique ID identifying this scripting `Engine`. + pub id: Option, + /// A module containing all functions directly loaded into the Engine. pub(crate) global_module: Module, /// A collection of all library packages loaded into the Engine. @@ -274,6 +277,8 @@ impl Default for Engine { fn default() -> Self { // Create the new scripting Engine let mut engine = Self { + id: None, + packages: Default::default(), global_module: Default::default(), @@ -431,6 +436,8 @@ impl Engine { /// Use the `load_package` method to load additional packages of functions. pub fn new_raw() -> Self { Self { + id: None, + packages: Default::default(), global_module: Default::default(), module_resolver: None, @@ -483,6 +490,15 @@ impl Engine { self.optimization_level = optimization_level } + /// The current optimization level. + /// It controls whether and how the `Engine` will optimize an AST after compilation. + /// + /// Not available under the `no_optimize` feature. + #[cfg(not(feature = "no_optimize"))] + pub fn optimization_level(&self) -> OptimizationLevel { + self.optimization_level + } + /// Set the maximum levels of function calls allowed for a script in order to avoid /// infinite recursion and stack overflows. #[cfg(not(feature = "unchecked"))] @@ -490,11 +506,27 @@ impl Engine { self.max_call_stack_depth = levels } + /// The maximum levels of function calls allowed for a script. + #[cfg(not(feature = "unchecked"))] + pub fn max_call_levels(&self) -> usize { + self.max_call_stack_depth + } + /// Set the maximum number of operations allowed for a script to run to avoid /// consuming too much resources (0 for unlimited). #[cfg(not(feature = "unchecked"))] pub fn set_max_operations(&mut self, operations: u64) { - self.max_operations = operations; + self.max_operations = if operations == u64::MAX { + 0 + } else { + operations + }; + } + + /// The maximum number of operations allowed for a script to run (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn max_operations(&self) -> u64 { + self.max_operations } /// Set the maximum number of imported modules allowed for a script. @@ -503,31 +535,77 @@ impl Engine { self.max_modules = modules; } - /// Set the depth limits for expressions/statements (0 for unlimited). + /// The maximum number of imported modules allowed for a script. + #[cfg(not(feature = "unchecked"))] + pub fn max_modules(&self) -> usize { + self.max_modules + } + + /// Set the depth limits for expressions (0 for unlimited). #[cfg(not(feature = "unchecked"))] pub fn set_max_expr_depths(&mut self, max_expr_depth: usize, max_function_expr_depth: usize) { - self.max_expr_depth = max_expr_depth; - self.max_function_expr_depth = max_function_expr_depth; + self.max_expr_depth = if max_expr_depth == usize::MAX { + 0 + } else { + max_expr_depth + }; + self.max_function_expr_depth = if max_function_expr_depth == usize::MAX { + 0 + } else { + max_function_expr_depth + }; + } + + /// The depth limit for expressions (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn max_expr_depth(&self) -> usize { + self.max_expr_depth + } + + /// The depth limit for expressions in functions (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn max_function_expr_depth(&self) -> usize { + self.max_function_expr_depth } /// Set the maximum length of strings (0 for unlimited). #[cfg(not(feature = "unchecked"))] pub fn set_max_string_size(&mut self, max_size: usize) { - self.max_string_size = max_size; + self.max_string_size = if max_size == usize::MAX { 0 } else { max_size }; + } + + /// The maximum length of strings (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn max_string_size(&self) -> usize { + self.max_string_size } /// Set the maximum length of arrays (0 for unlimited). #[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "no_index"))] pub fn set_max_array_size(&mut self, max_size: usize) { - self.max_array_size = max_size; + self.max_array_size = if max_size == usize::MAX { 0 } else { max_size }; + } + + /// The maximum length of arrays (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + #[cfg(not(feature = "no_index"))] + pub fn max_array_size(&self) -> usize { + self.max_array_size } /// Set the maximum length of object maps (0 for unlimited). #[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "no_object"))] pub fn set_max_map_size(&mut self, max_size: usize) { - self.max_map_size = max_size; + self.max_map_size = if max_size == usize::MAX { 0 } else { max_size }; + } + + /// The maximum length of object maps (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + #[cfg(not(feature = "no_object"))] + pub fn max_map_size(&self) -> usize { + self.max_map_size } /// Set the module resolution service used by the `Engine`. From ff37e024438ab78efe18e56e81816121fc00c3e4 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 16 Jun 2020 09:34:30 +0800 Subject: [PATCH 43/53] Add iterator support for strings. --- README.md | 16 +++++++++++++++- examples/repl.rs | 2 +- src/engine.rs | 24 ++++++++++++------------ src/packages/string_more.rs | 9 +++++++++ tests/for.rs | 20 ++++++++++++++++++++ 5 files changed, 57 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 78fbae94..401b49fc 100644 --- a/README.md +++ b/README.md @@ -1499,6 +1499,11 @@ record == "Bob X. Davis: age 42 ❤\n"; "Davis" in record == true; 'X' in record == true; 'C' in record == false; + +// Strings can be iterated with a 'for' statement, yielding characters +for ch in record { + print(ch); +} ``` The maximum allowed length of a string can be controlled via `Engine::set_max_string_size` @@ -2011,9 +2016,18 @@ loop { Iterating through a range or an [array] is provided by the `for` ... `in` loop. ```rust -let array = [1, 3, 5, 7, 9, 42]; +// Iterate through string, yielding characters +let s = "hello, world!"; + +for ch in s { + if ch > 'z' { continue; } // skip to the next iteration + print(ch); + if x == '@' { break; } // break out of for loop +} // Iterate through array +let array = [1, 3, 5, 7, 9, 42]; + for x in array { if x > 10 { continue; } // skip to the next iteration print(x); diff --git a/examples/repl.rs b/examples/repl.rs index 4a3bd561..f18a56e5 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -125,7 +125,7 @@ fn main() { match engine .compile_with_scope(&scope, &script) - .map_err(|err| err.into()) + .map_err(Into::into) .and_then(|r| { ast_u = r.clone(); diff --git a/src/engine.rs b/src/engine.rs index e1beb635..c94d1423 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -2265,14 +2265,14 @@ fn run_builtin_binary_op( #[cfg(not(feature = "unchecked"))] match op { - "+" => return add(x, y).map(Into::::into).map(Some), - "-" => return sub(x, y).map(Into::::into).map(Some), - "*" => return mul(x, y).map(Into::::into).map(Some), - "/" => return div(x, y).map(Into::::into).map(Some), - "%" => return modulo(x, y).map(Into::::into).map(Some), - "~" => return pow_i_i(x, y).map(Into::::into).map(Some), - ">>" => return shr(x, y).map(Into::::into).map(Some), - "<<" => return shl(x, y).map(Into::::into).map(Some), + "+" => return add(x, y).map(Into::into).map(Some), + "-" => return sub(x, y).map(Into::into).map(Some), + "*" => return mul(x, y).map(Into::into).map(Some), + "/" => return div(x, y).map(Into::into).map(Some), + "%" => return modulo(x, y).map(Into::into).map(Some), + "~" => return pow_i_i(x, y).map(Into::into).map(Some), + ">>" => return shr(x, y).map(Into::into).map(Some), + "<<" => return shl(x, y).map(Into::into).map(Some), _ => (), } @@ -2283,9 +2283,9 @@ fn run_builtin_binary_op( "*" => return Ok(Some((x * y).into())), "/" => return Ok(Some((x / y).into())), "%" => return Ok(Some((x % y).into())), - "~" => return pow_i_i_u(x, y).map(Into::::into).map(Some), - ">>" => return shr_u(x, y).map(Into::::into).map(Some), - "<<" => return shl_u(x, y).map(Into::::into).map(Some), + "~" => return pow_i_i_u(x, y).map(Into::into).map(Some), + ">>" => return shr_u(x, y).map(Into::into).map(Some), + "<<" => return shl_u(x, y).map(Into::into).map(Some), _ => (), } @@ -2359,7 +2359,7 @@ fn run_builtin_binary_op( "*" => return Ok(Some((x * y).into())), "/" => return Ok(Some((x / y).into())), "%" => return Ok(Some((x % y).into())), - "~" => return pow_f_f(x, y).map(Into::::into).map(Some), + "~" => return pow_f_f(x, y).map(Into::into).map(Some), "==" => return Ok(Some((x == y).into())), "!=" => return Ok(Some((x != y).into())), ">" => return Ok(Some((x > y).into())), diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 6b5928c1..4223b375 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -15,6 +15,7 @@ use crate::stdlib::{ fmt::Display, format, string::{String, ToString}, + vec::Vec, }; fn prepend(x: T, y: ImmutableString) -> FuncReturn { @@ -290,4 +291,12 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str Ok(()) }, ); + + // Register string iterator + lib.set_iter( + TypeId::of::(), + |arr| Box::new( + arr.cast::().chars().collect::>().into_iter().map(Into::into) + ) as Box>, + ); }); diff --git a/tests/for.rs b/tests/for.rs index cddff6ed..4f99c06f 100644 --- a/tests/for.rs +++ b/tests/for.rs @@ -30,6 +30,26 @@ fn test_for_array() -> Result<(), Box> { Ok(()) } +#[test] +fn test_for_string() -> Result<(), Box> { + let engine = Engine::new(); + + let script = r#" + let s = "hello"; + let sum = 0; + + for ch in s { + sum += ch.to_int(); + } + + sum + "#; + + assert_eq!(engine.eval::(script)?, 532); + + Ok(()) +} + #[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_index"))] #[test] From dc540755e71d184da1716d845637398a7f466977 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 16 Jun 2020 09:40:31 +0800 Subject: [PATCH 44/53] Revert plugins. --- src/engine.rs | 1 - src/fn_native.rs | 48 +------------------------- src/fn_register.rs | 86 ---------------------------------------------- src/lib.rs | 5 --- src/plugin.rs | 42 ---------------------- 5 files changed, 1 insertion(+), 181 deletions(-) delete mode 100644 src/plugin.rs diff --git a/src/engine.rs b/src/engine.rs index c94d1423..c3a7be8e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1783,7 +1783,6 @@ impl Engine { self.call_script_fn(&mut scope, state, lib, name, fn_def, args, level) .map_err(|err| EvalAltResult::new_position(err, *pos)) } - Ok(f) if f.is_plugin_fn() => f.get_plugin_fn().call(args.as_mut(), *pos), Ok(f) => { f.get_native_fn()(self, args.as_mut()).map_err(|err| err.new_position(*pos)) } diff --git a/src/fn_native.rs b/src/fn_native.rs index fc6e7673..b7805ff5 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -1,7 +1,6 @@ use crate::any::Dynamic; use crate::engine::Engine; use crate::parser::ScriptFnDef; -use crate::plugin::PluginFunction; use crate::result::EvalAltResult; use crate::stdlib::{boxed::Box, fmt, rc::Rc, sync::Arc}; @@ -60,11 +59,6 @@ pub type FnAny = pub type IteratorFn = fn(Dynamic) -> Box>; -#[cfg(feature = "sync")] -pub type SharedPluginFunction = Arc; -#[cfg(not(feature = "sync"))] -pub type SharedPluginFunction = Rc; - #[cfg(not(feature = "sync"))] pub type Callback = Box R + 'static>; #[cfg(feature = "sync")] @@ -80,8 +74,6 @@ pub enum CallableFunction { Method(Shared), /// An iterator function. Iterator(IteratorFn), - /// A plugin-defined function, - Plugin(SharedPluginFunction), /// A script-defined function. Script(Shared), } @@ -92,7 +84,6 @@ impl fmt::Debug for CallableFunction { Self::Pure(_) => write!(f, "NativePureFunction"), Self::Method(_) => write!(f, "NativeMethod"), Self::Iterator(_) => write!(f, "NativeIterator"), - Self::Plugin(_) => write!(f, "PluginFunction"), Self::Script(fn_def) => fmt::Debug::fmt(fn_def, f), } } @@ -104,7 +95,6 @@ impl CallableFunction { match self { Self::Pure(_) => true, Self::Method(_) | Self::Iterator(_) | Self::Script(_) => false, - Self::Plugin(_) => false, } } /// Is this a pure native Rust method-call? @@ -112,7 +102,6 @@ impl CallableFunction { match self { Self::Method(_) => true, Self::Pure(_) | Self::Iterator(_) | Self::Script(_) => false, - Self::Plugin(_) => false, } } /// Is this an iterator function? @@ -120,7 +109,6 @@ impl CallableFunction { match self { Self::Iterator(_) => true, Self::Pure(_) | Self::Method(_) | Self::Script(_) => false, - Self::Plugin(_) => false, } } /// Is this a Rhai-scripted function? @@ -128,14 +116,6 @@ impl CallableFunction { match self { Self::Script(_) => true, Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => false, - Self::Plugin(_) => false, - } - } - /// Is this a plugin-defined function? - pub fn is_plugin_fn(&self) -> bool { - match self { - Self::Plugin(_) => true, - Self::Pure(_) | Self::Method(_) | Self::Iterator(_) | Self::Script(_) => false, } } /// Get a reference to a native Rust function. @@ -147,7 +127,6 @@ impl CallableFunction { match self { Self::Pure(f) | Self::Method(f) => f.as_ref(), Self::Iterator(_) | Self::Script(_) => panic!(), - Self::Plugin(_) => panic!(), } } /// Get a shared reference to a script-defined function definition. @@ -157,7 +136,7 @@ impl CallableFunction { /// Panics if the `CallableFunction` is not `Script`. pub fn get_shared_fn_def(&self) -> Shared { match self { - Self::Pure(_) | Self::Method(_) | Self::Plugin(_) | Self::Iterator(_) => panic!(), + Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => panic!(), Self::Script(f) => f.clone(), } } @@ -170,7 +149,6 @@ impl CallableFunction { match self { Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => panic!(), Self::Script(f) => f, - Self::Plugin(_) => panic!(), } } /// Get a reference to an iterator function. @@ -182,18 +160,6 @@ impl CallableFunction { match self { Self::Iterator(f) => *f, Self::Pure(_) | Self::Method(_) | Self::Script(_) => panic!(), - Self::Plugin(_) => panic!(), - } - } - /// Get a reference to a plugin function. - /// - /// # Panics - /// - /// Panics if the `CallableFunction` is not `Plugin`. - pub fn get_plugin_fn<'s>(&'s self) -> SharedPluginFunction { - match self { - Self::Plugin(f) => f.clone(), - Self::Pure(_) | Self::Method(_) | Self::Script(_) | Self::Iterator(_) => panic!(), } } /// Create a new `CallableFunction::Pure`. @@ -204,18 +170,6 @@ impl CallableFunction { pub fn from_method(func: Box) -> Self { Self::Method(func.into()) } - - #[cfg(feature = "sync")] - /// Create a new `CallableFunction::Plugin`. - pub fn from_plugin(plugin: impl PluginFunction + 'static + Send + Sync) -> Self { - Self::Plugin(Arc::new(plugin)) - } - - #[cfg(not(feature = "sync"))] - /// Create a new `CallableFunction::Plugin`. - pub fn from_plugin(plugin: impl PluginFunction + 'static) -> Self { - Self::Plugin(Rc::new(plugin)) - } } impl From for CallableFunction { diff --git a/src/fn_register.rs b/src/fn_register.rs index cf83ae14..1b07cbe8 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -5,91 +5,11 @@ use crate::any::{Dynamic, Variant}; use crate::engine::Engine; use crate::fn_native::{CallableFunction, FnAny, FnCallArgs, SendSync}; use crate::parser::FnAccess; -use crate::plugin::Plugin; use crate::result::EvalAltResult; use crate::utils::ImmutableString; use crate::stdlib::{any::TypeId, boxed::Box, mem}; -/// A trait to register custom plugins with the `Engine`. -/// -/// A plugin consists of a number of functions. All functions will be registered with the engine. -pub trait RegisterPlugin { - /// Allow extensions of the engine's behavior. - /// - /// This can include importing modules, registering functions to the global name space, and - /// more. - /// - /// # Example - /// - /// ``` - /// use rhai::{FLOAT, INT, Module, ModuleResolver, RegisterFn, RegisterPlugin}; - /// use rhai::plugin::*; - /// use rhai::module_resolvers::*; - /// - /// // A function we want to expose to Rhai. - /// #[derive(Copy, Clone)] - /// struct DistanceFunction(); - /// - /// impl PluginFunction for DistanceFunction { - /// fn is_method_call(&self) -> bool { false } - /// fn is_varadic(&self) -> bool { false } - /// - /// fn call(&self, args: &[&mut Dynamic], pos: Position) -> Result> { - /// let x1: &FLOAT = args[0].downcast_ref::().unwrap(); - /// let y1: &FLOAT = args[1].downcast_ref::().unwrap(); - /// let x2: &FLOAT = args[2].downcast_ref::().unwrap(); - /// let y2: &FLOAT = args[3].downcast_ref::().unwrap(); - /// let square_sum = (y2 - y1).abs().powf(2.0) + (x2 -x1).abs().powf(2.0); - /// Ok(Dynamic::from(square_sum.sqrt())) - /// } - /// - /// fn clone_boxed(&self) -> Box { - /// Box::new(DistanceFunction()) - /// } - /// } - /// - /// // A simple custom plugin. This should not usually be done with hand-written code. - /// #[derive(Copy, Clone)] - /// pub struct AdvancedMathPlugin(); - /// - /// impl Plugin for AdvancedMathPlugin { - /// fn register_contents(self, engine: &mut Engine) { - /// // Plugins are allowed to have side-effects on the engine. - /// engine.register_fn("get_mystic_number", || { 42 as FLOAT }); - /// - /// // Main purpose: create a module to expose the functions to Rhai. - /// // - /// // This is currently a hack. There needs to be a better API here for "plugin" - /// // modules. - /// let mut m = Module::new(); - /// m.set_fn("euclidean_distance".to_string(), FnAccess::Public, - /// &[std::any::TypeId::of::(), - /// std::any::TypeId::of::(), - /// std::any::TypeId::of::(), - /// std::any::TypeId::of::()], - /// CallableFunction::from_plugin(DistanceFunction())); - /// let mut r = StaticModuleResolver::new(); - /// r.insert("Math::Advanced".to_string(), m); - /// engine.set_module_resolver(Some(r)); - /// } - /// } - /// - /// - /// # fn main() -> Result<(), Box> { - /// - /// let mut engine = Engine::new(); - /// engine.register_plugin(AdvancedMathPlugin()); - /// - /// assert_eq!(engine.eval::( - /// r#"import "Math::Advanced" as math; - /// let x = math::euclidean_distance(0.0, 1.0, 0.0, get_mystic_number()); x"#)?, 41.0); - /// # Ok(()) - /// # } - /// ``` - fn register_plugin(&mut self, plugin: PL); -} - /// Trait to register custom functions with the `Engine`. pub trait RegisterFn { /// Register a custom function with the `Engine`. @@ -191,12 +111,6 @@ pub fn by_value(data: &mut Dynamic) -> T { } } -impl RegisterPlugin for Engine { - fn register_plugin(&mut self, plugin: PL) { - plugin.register_contents(self); - } -} - /// This macro creates a closure wrapping a registered function. macro_rules! make_func { ($fn:ident : $map:expr ; $($par:ident => $convert:expr),*) => { diff --git a/src/lib.rs b/src/lib.rs index e0f2245c..d4dedda3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,10 +79,6 @@ pub mod fn_native; mod fn_register; mod module; mod optimize; -#[cfg(not(feature = "no_module"))] -pub mod plugin; -#[cfg(feature = "no_module")] -mod plugin; pub mod packages; mod parser; mod result; @@ -96,7 +92,6 @@ pub use any::Dynamic; pub use engine::Engine; pub use error::{ParseError, ParseErrorType}; pub use fn_register::{RegisterFn, RegisterResultFn}; -pub use fn_register::RegisterPlugin; pub use module::Module; pub use parser::{ImmutableString, AST, INT}; pub use result::EvalAltResult; diff --git a/src/plugin.rs b/src/plugin.rs deleted file mode 100644 index 8159d3c8..00000000 --- a/src/plugin.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Module defining plugins in Rhai. Is exported for use by plugin authors. - -pub use crate::Engine; -pub use crate::any::{Dynamic, Variant}; -pub use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn}; -pub use crate::parser::{ - FnAccess, - FnAccess::{Private, Public}, - AST, -}; -pub use crate::result::EvalAltResult; -pub use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; -pub use crate::token::{Position, Token}; -pub use crate::utils::StaticVec; - -#[cfg(features = "sync")] -/// Represents an externally-written plugin for the Rhai interpreter. -/// -/// This trait should not be used directly. Use the `#[plugin]` procedural attribute instead. -pub trait Plugin: Send { - fn register_contents(self, engine: &mut Engine); -} - -#[cfg(not(features = "sync"))] -/// Represents an externally-written plugin for the Rhai interpreter. -/// -/// This trait should not be used directly. Use the `#[plugin]` procedural attribute instead. -pub trait Plugin: Send + Sync { - fn register_contents(self, engine: &mut Engine); -} - -/// Represents a function that is statically defined within a plugin. -/// -/// This trait should not be used directly. Use the `#[plugin]` procedural attribute instead. -pub trait PluginFunction { - fn is_method_call(&self) -> bool; - fn is_varadic(&self) -> bool; - - fn call(&self, args: &[&mut Dynamic], pos: Position) -> Result>; - - fn clone_boxed(&self) -> Box; -} From 1adf3cc39a6c49eeaa5ebdda702587ed3afd8e76 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 16 Jun 2020 22:14:46 +0800 Subject: [PATCH 45/53] Support for trailing commas. --- README.md | 11 +- RELEASES.md | 1 + src/fn_native.rs | 8 +- src/parser.rs | 249 +++++++++++++++++++++++-------------------- src/scope.rs | 2 +- src/token.rs | 10 +- tests/arrays.rs | 1 + tests/call_fn.rs | 4 +- tests/functions.rs | 5 + tests/internal_fn.rs | 6 ++ tests/maps.rs | 4 + 11 files changed, 173 insertions(+), 128 deletions(-) diff --git a/README.md b/README.md index 401b49fc..2ee5e6b3 100644 --- a/README.md +++ b/README.md @@ -1607,6 +1607,8 @@ The following methods (mostly defined in the [`BasicArrayPackage`](#packages) bu ```rust let y = [2, 3]; // array literal with 2 elements +let y = [2, 3,]; // trailing comma is OK + y.insert(0, 1); // insert element at the beginning y.insert(999, 4); // insert element at the end @@ -1751,6 +1753,8 @@ ts.obj = y; // object maps can be assigned completely (by value copy let foo = ts.list.a; foo == 42; +let foo = #{ a:1,}; // trailing comma is OK + let foo = #{ a:1, b:2, c:3 }["a"]; foo == 1; @@ -2115,7 +2119,12 @@ fn add(x, y) { return x + y; } -print(add(2, 3)); +fn sub(x, y,) { // trailing comma in parameters list is OK + return x - y; +} + +print(add(2, 3)); // prints 5 +print(sub(2, 3,)); // prints -1 - trailing comma in arguments list is OK ``` ### Implicit return diff --git a/RELEASES.md b/RELEASES.md index b19caf44..6fec7a48 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -22,6 +22,7 @@ New features * Indexers are now split into getters and setters (which now support updates). The API is split into `Engine::register_indexer_get` and `Engine::register_indexer_set` with `Engine::register_indexer_get_set` being a shorthand. Similarly, `Module::set_indexer_get_fn` and `Module::set_indexer_set_fn` are added. * `Engine:register_fn` and `Engine:register_result_fn` accepts functions that take parameters of type `&str` (immutable string slice), which maps directly to `ImmutableString`. This is to avoid needing wrappers for functions taking string parameters. * Set maximum limit on data sizes: `Engine::set_max_string_size`, `Engine::set_max_array_size` and `Engine::set_max_map_size`. +* Supports trailing commas on array literals, object map literals, function definitions and function calls. Version 0.15.0 diff --git a/src/fn_native.rs b/src/fn_native.rs index b7805ff5..6c9eba10 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -126,7 +126,7 @@ impl CallableFunction { pub fn get_native_fn(&self) -> &FnAny { match self { Self::Pure(f) | Self::Method(f) => f.as_ref(), - Self::Iterator(_) | Self::Script(_) => panic!(), + Self::Iterator(_) | Self::Script(_) => unreachable!(), } } /// Get a shared reference to a script-defined function definition. @@ -136,7 +136,7 @@ impl CallableFunction { /// Panics if the `CallableFunction` is not `Script`. pub fn get_shared_fn_def(&self) -> Shared { match self { - Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => panic!(), + Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => unreachable!(), Self::Script(f) => f.clone(), } } @@ -147,7 +147,7 @@ impl CallableFunction { /// Panics if the `CallableFunction` is not `Script`. pub fn get_fn_def(&self) -> &ScriptFnDef { match self { - Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => panic!(), + Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => unreachable!(), Self::Script(f) => f, } } @@ -159,7 +159,7 @@ impl CallableFunction { pub fn get_iter_fn(&self) -> IteratorFn { match self { Self::Iterator(f) => *f, - Self::Pure(_) | Self::Method(_) | Self::Script(_) => panic!(), + Self::Pure(_) | Self::Method(_) | Self::Script(_) => unreachable!(), } } /// Create a new `CallableFunction::Pure`. diff --git a/src/parser.rs b/src/parser.rs index acbb59e5..5d0bd902 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -518,7 +518,7 @@ impl Expr { ))) } - _ => panic!("cannot get value of non-constant expression"), + _ => unreachable!("cannot get value of non-constant expression"), } } @@ -543,7 +543,7 @@ impl Expr { Self::Array(x) if x.0.iter().all(Self::is_constant) => "array".to_string(), - _ => panic!("cannot get value of non-constant expression"), + _ => unreachable!("cannot get value of non-constant expression"), } } @@ -728,7 +728,7 @@ fn eat_token(input: &mut TokenStream, token: Token) -> Position { let (t, pos) = input.next().unwrap(); if t != token { - panic!( + unreachable!( "expecting {} (found {}) at {}", token.syntax(), t.syntax(), @@ -836,7 +836,11 @@ fn parse_call_expr( let settings = settings.level_up(); loop { - args.push(parse_expr(input, state, settings)?); + match input.peek().unwrap() { + // id(...args, ) - handle trailing comma + (Token::RightParen, _) => (), + _ => args.push(parse_expr(input, state, settings)?), + } match input.peek().unwrap() { // id(...args) @@ -1082,42 +1086,47 @@ fn parse_array_literal( let mut arr = StaticVec::new(); - if !match_token(input, Token::RightBracket)? { - while !input.peek().unwrap().0.is_eof() { - if state.max_array_size > 0 && arr.len() >= state.max_array_size { - return Err(PERR::LiteralTooLarge( - "Size of array literal".to_string(), - state.max_array_size, - ) - .into_err(input.peek().unwrap().1)); - } - - let expr = parse_expr(input, state, settings.level_up())?; - arr.push(expr); - - match input.peek().unwrap() { - (Token::Comma, _) => eat_token(input, Token::Comma), - (Token::RightBracket, _) => { - eat_token(input, Token::RightBracket); - break; - } - (Token::EOF, pos) => { - return Err(PERR::MissingToken( - Token::RightBracket.into(), - "to end this array literal".into(), - ) - .into_err(*pos)) - } - (Token::LexError(err), pos) => return Err(err.into_err(*pos)), - (_, pos) => { - return Err(PERR::MissingToken( - Token::Comma.into(), - "to separate the items of this array literal".into(), - ) - .into_err(*pos)) - } - }; + while !input.peek().unwrap().0.is_eof() { + if state.max_array_size > 0 && arr.len() >= state.max_array_size { + return Err(PERR::LiteralTooLarge( + "Size of array literal".to_string(), + state.max_array_size, + ) + .into_err(input.peek().unwrap().1)); } + + match input.peek().unwrap() { + (Token::RightBracket, _) => { + eat_token(input, Token::RightBracket); + break; + } + _ => { + let expr = parse_expr(input, state, settings.level_up())?; + arr.push(expr); + } + } + + match input.peek().unwrap() { + (Token::Comma, _) => { + eat_token(input, Token::Comma); + } + (Token::RightBracket, _) => (), + (Token::EOF, pos) => { + return Err(PERR::MissingToken( + Token::RightBracket.into(), + "to end this array literal".into(), + ) + .into_err(*pos)) + } + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), + (_, pos) => { + return Err(PERR::MissingToken( + Token::Comma.into(), + "to separate the items of this array literal".into(), + ) + .into_err(*pos)) + } + }; } Ok(Expr::Array(Box::new((arr, settings.pos)))) @@ -1133,77 +1142,82 @@ fn parse_map_literal( let mut map = StaticVec::new(); - if !match_token(input, Token::RightBrace)? { - while !input.peek().unwrap().0.is_eof() { - const MISSING_RBRACE: &str = "to end this object map literal"; + while !input.peek().unwrap().0.is_eof() { + const MISSING_RBRACE: &str = "to end this object map literal"; - let (name, pos) = match input.next().unwrap() { - (Token::Identifier(s), pos) => (s, pos), - (Token::StringConst(s), pos) => (s, pos), - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (_, pos) if map.is_empty() => { - return Err( - PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) - .into_err(pos), - ) - } - (Token::EOF, pos) => { - return Err( - PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) - .into_err(pos), - ) - } - (_, pos) => return Err(PERR::PropertyExpected.into_err(pos)), - }; - - match input.next().unwrap() { - (Token::Colon, _) => (), - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (_, pos) => { - return Err(PERR::MissingToken( - Token::Colon.into(), - format!( - "to follow the property '{}' in this object map literal", - name - ), - ) - .into_err(pos)) - } - }; - - if state.max_map_size > 0 && map.len() >= state.max_map_size { - return Err(PERR::LiteralTooLarge( - "Number of properties in object map literal".to_string(), - state.max_map_size, - ) - .into_err(input.peek().unwrap().1)); + match input.peek().unwrap() { + (Token::RightBrace, _) => { + eat_token(input, Token::RightBrace); + break; } + _ => { + let (name, pos) = match input.next().unwrap() { + (Token::Identifier(s), pos) => (s, pos), + (Token::StringConst(s), pos) => (s, pos), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (_, pos) if map.is_empty() => { + return Err(PERR::MissingToken( + Token::RightBrace.into(), + MISSING_RBRACE.into(), + ) + .into_err(pos)) + } + (Token::EOF, pos) => { + return Err(PERR::MissingToken( + Token::RightBrace.into(), + MISSING_RBRACE.into(), + ) + .into_err(pos)) + } + (_, pos) => return Err(PERR::PropertyExpected.into_err(pos)), + }; - let expr = parse_expr(input, state, settings.level_up())?; - map.push(((name, pos), expr)); + match input.next().unwrap() { + (Token::Colon, _) => (), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (_, pos) => { + return Err(PERR::MissingToken( + Token::Colon.into(), + format!( + "to follow the property '{}' in this object map literal", + name + ), + ) + .into_err(pos)) + } + }; - match input.peek().unwrap() { - (Token::Comma, _) => { - eat_token(input, Token::Comma); - } - (Token::RightBrace, _) => { - eat_token(input, Token::RightBrace); - break; - } - (Token::Identifier(_), pos) => { - return Err(PERR::MissingToken( - Token::Comma.into(), - "to separate the items of this object map literal".into(), - ) - .into_err(*pos)) - } - (Token::LexError(err), pos) => return Err(err.into_err(*pos)), - (_, pos) => { - return Err( - PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) - .into_err(*pos), + if state.max_map_size > 0 && map.len() >= state.max_map_size { + return Err(PERR::LiteralTooLarge( + "Number of properties in object map literal".to_string(), + state.max_map_size, ) + .into_err(input.peek().unwrap().1)); } + + let expr = parse_expr(input, state, settings.level_up())?; + map.push(((name, pos), expr)); + } + } + + match input.peek().unwrap() { + (Token::Comma, _) => { + eat_token(input, Token::Comma); + } + (Token::RightBrace, _) => (), + (Token::Identifier(_), pos) => { + return Err(PERR::MissingToken( + Token::Comma.into(), + "to separate the items of this object map literal".into(), + ) + .into_err(*pos)) + } + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), + (_, pos) => { + return Err( + PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) + .into_err(*pos), + ) } } } @@ -1309,7 +1323,7 @@ fn parse_primary( parse_index_chain(input, state, expr, settings.level_up())? } // Unknown postfix operator - (expr, token) => panic!( + (expr, token) => unreachable!( "unknown postfix operator '{}' for {:?}", token.syntax(), expr @@ -2290,7 +2304,7 @@ fn parse_stmt( let return_type = match input.next().unwrap() { (Token::Return, _) => ReturnType::Return, (Token::Throw, _) => ReturnType::Exception, - _ => panic!("token should be return or throw"), + _ => unreachable!(), }; match input.peek().unwrap() { @@ -2356,15 +2370,20 @@ fn parse_fn( let sep_err = format!("to separate the parameters of function '{}'", name); loop { - match input.next().unwrap() { - (Token::Identifier(s), pos) => { - state.push((s.clone(), ScopeEntryType::Normal)); - params.push((s, pos)) - } - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (_, pos) => { - return Err(PERR::MissingToken(Token::RightParen.into(), end_err).into_err(pos)) - } + match input.peek().unwrap() { + (Token::RightParen, _) => (), + _ => match input.next().unwrap() { + (Token::Identifier(s), pos) => { + state.push((s.clone(), ScopeEntryType::Normal)); + params.push((s, pos)) + } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (_, pos) => { + return Err( + PERR::MissingToken(Token::RightParen.into(), end_err).into_err(pos) + ) + } + }, } match input.next().unwrap() { diff --git a/src/scope.rs b/src/scope.rs index bb15b777..97d3770e 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -417,7 +417,7 @@ impl<'a> Scope<'a> { pub fn set_value(&mut self, name: &'a str, value: T) { match self.get_index(name) { None => self.push(name, value), - Some((_, EntryType::Constant)) => panic!("variable {} is constant", name), + Some((_, EntryType::Constant)) => unreachable!("variable {} is constant", name), Some((index, EntryType::Normal)) => { self.0.get_mut(index).unwrap().value = Dynamic::from(value) } diff --git a/src/token.rs b/src/token.rs index 0bb0cbe3..d5f84902 100644 --- a/src/token.rs +++ b/src/token.rs @@ -294,7 +294,7 @@ impl Token { Export => "export", As => "as", EOF => "{EOF}", - _ => panic!("operator should be match in outer scope"), + _ => unreachable!("operator should be match in outer scope"), }) .into(), } @@ -548,7 +548,7 @@ impl<'a> TokenIterator<'a> { 'x' => 2, 'u' => 4, 'U' => 8, - _ => panic!("should be 'x', 'u' or 'U'"), + _ => unreachable!(), }; for _ in 0..len { @@ -667,14 +667,14 @@ impl<'a> TokenIterator<'a> { '0', '1', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', ], - _ => panic!("unexpected character {}", ch), + _ => unreachable!(), }; radix_base = Some(match ch { 'x' | 'X' => 16, 'o' | 'O' => 8, 'b' | 'B' => 2, - _ => panic!("unexpected character {}", ch), + _ => unreachable!(), }); while let Some(next_char_in_hex) = self.peek_next() { @@ -1077,7 +1077,7 @@ impl<'a> TokenIterator<'a> { } ('~', _) => return Some((Token::PowerOf, pos)), - ('\0', _) => panic!("should not be EOF"), + ('\0', _) => unreachable!(), (ch, _) if ch.is_whitespace() => (), (ch, _) => return Some((Token::LexError(Box::new(LERR::UnexpectedChar(ch))), pos)), diff --git a/tests/arrays.rs b/tests/arrays.rs index 741156e9..8ea61cdf 100644 --- a/tests/arrays.rs +++ b/tests/arrays.rs @@ -6,6 +6,7 @@ fn test_arrays() -> Result<(), Box> { let engine = Engine::new(); assert_eq!(engine.eval::("let x = [1, 2, 3]; x[1]")?, 2); + assert_eq!(engine.eval::("let x = [1, 2, 3,]; x[1]")?, 2); assert_eq!(engine.eval::("let y = [1, 2, 3]; y[1] = 5; y[1]")?, 5); assert_eq!( engine.eval::(r#"let y = [1, [ 42, 88, "93" ], 3]; y[1][2][1]"#)?, diff --git a/tests/call_fn.rs b/tests/call_fn.rs index 29f7abca..dd46450d 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -68,7 +68,7 @@ fn test_call_fn_private() -> Result<(), Box> { let r: INT = engine.call_fn(&mut scope, &ast, "add", (40 as INT, 2 as INT))?; assert_eq!(r, 42); - let ast = engine.compile("private fn add(x, n) { x + n }")?; + let ast = engine.compile("private fn add(x, n, ) { x + n }")?; assert!(matches!( *engine.call_fn::<_, INT>(&mut scope, &ast, "add", (40 as INT, 2 as INT)) @@ -83,7 +83,7 @@ fn test_call_fn_private() -> Result<(), Box> { fn test_anonymous_fn() -> Result<(), Box> { let calc_func = Func::<(INT, INT, INT), INT>::create_from_script( Engine::new(), - "fn calc(x, y, z) { (x + y) * z }", + "fn calc(x, y, z,) { (x + y) * z }", "calc", )?; diff --git a/tests/functions.rs b/tests/functions.rs index ae12e052..6a94256a 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -7,6 +7,11 @@ fn test_functions() -> Result<(), Box> { assert_eq!(engine.eval::("fn add(x, n) { x + n } add(40, 2)")?, 42); + assert_eq!( + engine.eval::("fn add(x, n,) { x + n } add(40, 2,)")?, + 42 + ); + assert_eq!( engine.eval::("fn add(x, n) { x + n } let a = 40; add(a, 2); a")?, 40 diff --git a/tests/internal_fn.rs b/tests/internal_fn.rs index 764cf601..67417344 100644 --- a/tests/internal_fn.rs +++ b/tests/internal_fn.rs @@ -10,6 +10,12 @@ fn test_internal_fn() -> Result<(), Box> { engine.eval::("fn add_me(a, b) { a+b } add_me(3, 4)")?, 7 ); + + assert_eq!( + engine.eval::("fn add_me(a, b,) { a+b } add_me(3, 4,)")?, + 7 + ); + assert_eq!(engine.eval::("fn bob() { return 4; 5 } bob()")?, 4); Ok(()) diff --git a/tests/maps.rs b/tests/maps.rs index a7aef8dd..8061f1ed 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -12,6 +12,10 @@ fn test_map_indexing() -> Result<(), Box> { engine.eval::(r#"let x = #{a: 1, b: 2, c: 3}; x["b"]"#)?, 2 ); + assert_eq!( + engine.eval::(r#"let x = #{a: 1, b: 2, c: 3,}; x["b"]"#)?, + 2 + ); assert_eq!( engine.eval::( r#" From 151cd1af48210fd62f63f0ee8d8c3ad6b6cc68d9 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 16 Jun 2020 23:47:31 +0800 Subject: [PATCH 46/53] Support compiling to WASM. --- README.md | 9 ++++++--- RELEASES.md | 2 +- src/any.rs | 8 ++++++++ src/api.rs | 2 ++ src/packages/pkg_std.rs | 4 ++++ src/packages/time_basic.rs | 3 +++ tests/string.rs | 14 ++++++++++++++ 7 files changed, 38 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2ee5e6b3..d25637cd 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Features * Rugged - protection against malicious attacks (such as [stack-overflow](#maximum-call-stack-depth), [over-sized data](#maximum-length-of-strings), and [runaway scripts](#maximum-number-of-operations) etc.) that may come from untrusted third-party user-land scripts. * Track script evaluation [progress](#tracking-progress-and-force-terminate-script-run) and manually terminate a script run. * [`no-std`](#optional-features) support. +* Supports compiling to `WASM`, optionally with [minimal builds](#minimal-builds). * [Function overloading](#function-overloading). * [Operator overloading](#operator-overloading). * Organize code base with dynamically-loadable [modules]. @@ -141,8 +142,8 @@ Making [`Dynamic`] small helps performance due to better cache efficiency. ### Minimal builds -In order to compile a _minimal_build - i.e. a build optimized for size - perhaps for embedded targets, it is essential that -the correct linker flags are used in `cargo.toml`: +In order to compile a _minimal_build - i.e. a build optimized for size - perhaps for `no-std` embedded targets or for +compiling to `WASM`, it is essential that the correct linker flags are used in `cargo.toml`: ```toml [profile.release] @@ -156,7 +157,9 @@ all code is compiled in as what a script requires cannot be predicted. If a lang omitting them via special features is a prudent strategy to optimize the build for size. Omitting arrays (`no_index`) yields the most code-size savings, followed by floating-point support -(`no_float`), checked arithmetic (`unchecked`) and finally object maps and custom types (`no_object`). +(`no_float`), checked arithmetic/script resource limits (`unchecked`) and finally object maps and custom types (`no_object`). + +Where the usage scenario does not call for loading externally-defined modules, use `no_module` to save some bytes. Disable script-defined functions (`no_function`) only when the feature is not needed because code size savings is minimal. [`Engine::new_raw`](#raw-engine) creates a _raw_ engine. diff --git a/RELEASES.md b/RELEASES.md index 6fec7a48..d9cd5309 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -23,7 +23,7 @@ New features * `Engine:register_fn` and `Engine:register_result_fn` accepts functions that take parameters of type `&str` (immutable string slice), which maps directly to `ImmutableString`. This is to avoid needing wrappers for functions taking string parameters. * Set maximum limit on data sizes: `Engine::set_max_string_size`, `Engine::set_max_array_size` and `Engine::set_max_map_size`. * Supports trailing commas on array literals, object map literals, function definitions and function calls. - +* Supports compiling to `WASM`. Version 0.15.0 ============== diff --git a/src/any.rs b/src/any.rs index a50d6820..ec25a545 100644 --- a/src/any.rs +++ b/src/any.rs @@ -24,6 +24,8 @@ use crate::stdlib::{ }; #[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] +#[cfg(not(target_arch = "wasm64"))] use crate::stdlib::time::Instant; /// Trait to represent any type. @@ -192,6 +194,8 @@ impl Dynamic { Union::Module(_) => "sub-scope", #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] Union::Variant(value) if value.is::() => "timestamp", Union::Variant(value) => (***value).type_name(), } @@ -215,6 +219,8 @@ impl fmt::Display for Dynamic { Union::Module(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] Union::Variant(value) if value.is::() => write!(f, ""), Union::Variant(_) => write!(f, "?"), } @@ -238,6 +244,8 @@ impl fmt::Debug for Dynamic { Union::Module(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] Union::Variant(value) if value.is::() => write!(f, ""), Union::Variant(_) => write!(f, ""), } diff --git a/src/api.rs b/src/api.rs index 6a6dbf5c..4a6e04cc 100644 --- a/src/api.rs +++ b/src/api.rs @@ -27,6 +27,8 @@ use crate::stdlib::{ }; #[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] +#[cfg(not(target_arch = "wasm64"))] use crate::stdlib::{fs::File, io::prelude::*, path::PathBuf}; /// Engine public API diff --git a/src/packages/pkg_std.rs b/src/packages/pkg_std.rs index d2790d50..80179f43 100644 --- a/src/packages/pkg_std.rs +++ b/src/packages/pkg_std.rs @@ -6,6 +6,8 @@ use super::math_basic::BasicMathPackage; use super::pkg_core::CorePackage; use super::string_more::MoreStringPackage; #[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] +#[cfg(not(target_arch = "wasm64"))] use super::time_basic::BasicTimePackage; use crate::def_package; @@ -18,6 +20,8 @@ def_package!(crate:StandardPackage:"_Standard_ package containing all built-in f #[cfg(not(feature = "no_object"))] BasicMapPackage::init(lib); #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] BasicTimePackage::init(lib); MoreStringPackage::init(lib); }); diff --git a/src/packages/time_basic.rs b/src/packages/time_basic.rs index b8e13604..098c417d 100644 --- a/src/packages/time_basic.rs +++ b/src/packages/time_basic.rs @@ -1,3 +1,6 @@ +#![cfg(not(target_arch = "wasm32"))] +#![cfg(not(target_arch = "wasm64"))] + use super::logic::{eq, gt, gte, lt, lte, ne}; use super::math_basic::MAX_INT; diff --git a/tests/string.rs b/tests/string.rs index 6eed9e6a..e6b77841 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -159,6 +159,20 @@ fn test_string_substring() -> Result<(), Box> { fn test_string_fn() -> Result<(), Box> { let mut engine = Engine::new(); + engine.register_fn("set_to_x", |ch: &mut char| *ch = 'X'); + + #[cfg(not(feature = "no_index"))] + #[cfg(not(feature = "no_object"))] + assert_eq!( + engine.eval::(r#"let x="foo"; x[0].set_to_x(); x"#)?, + "Xoo" + ); + #[cfg(not(feature = "no_index"))] + assert_eq!( + engine.eval::(r#"let x="foo"; set_to_x(x[0]); x"#)?, + "foo" + ); + engine.register_fn("foo1", |s: &str| s.len() as INT); engine.register_fn("foo2", |s: ImmutableString| s.len() as INT); engine.register_fn("foo3", |s: String| s.len() as INT); From b6e1f652b669d38055edc0ce105be95695ae1958 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 16 Jun 2020 23:49:11 +0800 Subject: [PATCH 47/53] Fix bug when setting character in string via a method call. --- RELEASES.md | 5 ++++ src/engine.rs | 64 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index d9cd5309..fcd99454 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -7,6 +7,11 @@ Version 0.15.1 This is a minor release which enables updating indexers (via registered indexer setters) and supports functions with `&str` parameters (maps transparently to `ImmutableString`). +Buf fix +------- + +* `let s="abc"; s[1].change_to('X');` now correctly sets the character '`X`' into '`s`' yielding `"aXc"`. + Breaking changes ---------------- diff --git a/src/engine.rs b/src/engine.rs index c3a7be8e..66f04f3b 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,6 +1,6 @@ //! Main module defining the script evaluation `Engine`. -use crate::any::{Dynamic, Union}; +use crate::any::{Dynamic, Union, Variant}; use crate::calc_fn_hash; use crate::error::ParseErrorType; use crate::fn_native::{CallableFunction, Callback, FnCallArgs}; @@ -105,6 +105,14 @@ impl Target<'_> { Self::StringChar(_, _, _) => false, } } + /// Is the `Target` a specific type? + pub fn is(&self) -> bool { + match self { + Target::Ref(r) => r.is::(), + Target::Value(r) => r.is::(), + Target::StringChar(_, _, _) => TypeId::of::() == TypeId::of::(), + } + } /// Get the value of the `Target` as a `Dynamic`, cloning a referenced value if necessary. pub fn clone_into_dynamic(self) -> Dynamic { match self { @@ -817,7 +825,7 @@ impl Engine { .map(|name| if name.is::() { "&str | ImmutableString" } else { - self.map_type_name(name.type_name()) + self.map_type_name((*name).type_name()) }) .collect::>() .join(", ") @@ -1006,9 +1014,7 @@ impl Engine { mut new_val: Option, ) -> Result<(Dynamic, bool), Box> { let is_ref = target.is_ref(); - - // Get a reference to the mutation target Dynamic - let obj = target.as_mut(); + let is_value = target.is_value(); // Pop the last index value let mut idx_val = idx_values.pop(); @@ -1090,24 +1096,38 @@ impl Engine { let ((name, native, pos), _, hash, _, def_val) = x.as_ref(); let def_val = def_val.as_ref(); - let mut arg_values: StaticVec<_> = once(obj) - .chain( - idx_val - .downcast_mut::>() - .unwrap() - .iter_mut(), - ) - .collect(); - let args = arg_values.as_mut(); + // Get a reference to the mutation target Dynamic + let (result, updated) = { + let obj = target.as_mut(); + let mut arg_values: StaticVec<_> = once(obj) + .chain( + idx_val + .downcast_mut::>() + .unwrap() + .iter_mut(), + ) + .collect(); + let args = arg_values.as_mut(); - self.exec_fn_call(state, lib, name, *native, *hash, args, is_ref, def_val, 0) - .map_err(|err| EvalAltResult::new_position(err, *pos)) + self.exec_fn_call( + state, lib, name, *native, *hash, args, is_ref, def_val, 0, + ) + .map_err(|err| EvalAltResult::new_position(err, *pos))? + }; + + // Feed the changed temp value back + if updated && !is_ref && !is_value { + let new_val = target.as_mut().clone(); + target.set_value(new_val)?; + } + + Ok((result, updated)) } // xxx.module::fn_name(...) - syntax error Expr::FnCall(_) => unreachable!(), // {xxx:map}.id = ??? #[cfg(not(feature = "no_object"))] - Expr::Property(x) if obj.is::() && new_val.is_some() => { + Expr::Property(x) if target.is::() && new_val.is_some() => { let ((prop, _, _), pos) = x.as_ref(); let index = prop.clone().into(); let mut val = self.get_indexed_mut(state, lib, target, index, *pos, true)?; @@ -1118,7 +1138,7 @@ impl Engine { } // {xxx:map}.id #[cfg(not(feature = "no_object"))] - Expr::Property(x) if obj.is::() => { + Expr::Property(x) if target.is::() => { let ((prop, _, _), pos) = x.as_ref(); let index = prop.clone().into(); let val = self.get_indexed_mut(state, lib, target, index, *pos, false)?; @@ -1128,7 +1148,7 @@ impl Engine { // xxx.id = ??? Expr::Property(x) if new_val.is_some() => { let ((_, _, setter), pos) = x.as_ref(); - let mut args = [obj, new_val.as_mut().unwrap()]; + let mut args = [target.as_mut(), new_val.as_mut().unwrap()]; self.exec_fn_call(state, lib, setter, true, 0, &mut args, is_ref, None, 0) .map(|(v, _)| (v, true)) .map_err(|err| EvalAltResult::new_position(err, *pos)) @@ -1136,14 +1156,14 @@ impl Engine { // xxx.id Expr::Property(x) => { let ((_, getter, _), pos) = x.as_ref(); - let mut args = [obj]; + let mut args = [target.as_mut()]; self.exec_fn_call(state, lib, getter, true, 0, &mut args, is_ref, None, 0) .map(|(v, _)| (v, false)) .map_err(|err| EvalAltResult::new_position(err, *pos)) } #[cfg(not(feature = "no_object"))] // {xxx:map}.prop[expr] | {xxx:map}.prop.expr - Expr::Index(x) | Expr::Dot(x) if obj.is::() => { + Expr::Index(x) | Expr::Dot(x) if target.is::() => { let (prop, expr, pos) = x.as_ref(); let is_idx = matches!(rhs, Expr::Index(_)); @@ -1164,7 +1184,7 @@ impl Engine { Expr::Index(x) | Expr::Dot(x) => { let (prop, expr, pos) = x.as_ref(); let is_idx = matches!(rhs, Expr::Index(_)); - let args = &mut [obj, &mut Default::default()]; + let args = &mut [target.as_mut(), &mut Default::default()]; let (mut val, updated) = if let Expr::Property(p) = prop { let ((_, getter, _), _) = p.as_ref(); From 7f4f737ff2b7b1c7f0b81d61384fdea69fd320ae Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 17 Jun 2020 09:54:17 +0800 Subject: [PATCH 48/53] Gate WASM target. --- README.md | 36 ++++++++++++++++++++++++++---------- RELEASES.md | 4 ++-- src/api.rs | 14 ++++++++++++++ src/engine.rs | 11 ++++++++++- src/lib.rs | 2 ++ src/module.rs | 4 ++++ src/packages/mod.rs | 2 ++ src/result.rs | 14 +++++++++++++- tests/time.rs | 2 ++ 9 files changed, 75 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index d25637cd..34682ab7 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,11 @@ Rhai - Embedded Scripting for Rust Rhai is an embedded scripting language and evaluation engine for Rust that gives a safe and easy way to add scripting to any application. +Supported targets +----------------- + +* All common targets, including [WASM] and `no-std`. + Features -------- @@ -27,13 +32,11 @@ Features * Sand-boxed - the scripting [`Engine`], if declared immutable, cannot mutate the containing environment unless explicitly permitted (e.g. via a `RefCell`). * Rugged - protection against malicious attacks (such as [stack-overflow](#maximum-call-stack-depth), [over-sized data](#maximum-length-of-strings), and [runaway scripts](#maximum-number-of-operations) etc.) that may come from untrusted third-party user-land scripts. * Track script evaluation [progress](#tracking-progress-and-force-terminate-script-run) and manually terminate a script run. -* [`no-std`](#optional-features) support. -* Supports compiling to `WASM`, optionally with [minimal builds](#minimal-builds). * [Function overloading](#function-overloading). * [Operator overloading](#operator-overloading). * Organize code base with dynamically-loadable [modules]. * Scripts are [optimized](#script-optimization) (useful for template-based machine-generated scripts) for repeated evaluations. -* Support for [minimal builds](#minimal-builds) by excluding unneeded language [features](#optional-features). +* Support for [minimal builds] by excluding unneeded language [features](#optional-features). * Very few additional dependencies (right now only [`num-traits`](https://crates.io/crates/num-traits/) to do checked arithmetic operations); for [`no-std`](#optional-features) builds, a number of additional dependencies are pulled in to provide for functionalities that used to be in `std`. @@ -142,8 +145,10 @@ Making [`Dynamic`] small helps performance due to better cache efficiency. ### Minimal builds +[minimal builds]: #minimal-builds + In order to compile a _minimal_build - i.e. a build optimized for size - perhaps for `no-std` embedded targets or for -compiling to `WASM`, it is essential that the correct linker flags are used in `cargo.toml`: +compiling to [WASM], it is essential that the correct linker flags are used in `cargo.toml`: ```toml [profile.release] @@ -167,8 +172,19 @@ A _raw_ engine supports, out of the box, only a very [restricted set](#built-in- Selectively include other necessary functionalities by loading specific [packages] to minimize the footprint. Packages are sharable (even across threads via the [`sync`] feature), so they only have to be created once. -Related -------- +### Compiling to WebAssembly (WASM) + +[WASM]: #compiling-to-WebAssembly-wasm + +It is possible to use Rhai when compiling to WebAssembly (WASM), but certain features will not be available, +such as the script file API's and loading modules from external script files. + +Also look into [minimal builds] to reduce generated WASM size. As of this version, a typical, full-featured +Rhai scripting engine compiles to a single WASM file around 200KB gzipped. When excluding features that are +marginal in WASM environment, the gzipped payload can be further shrunk to 160KB. + +Related Resources +----------------- Other cool projects to check out: @@ -2453,10 +2469,10 @@ which simply loads a script file based on the path (with `.rhai` extension attac Built-in module resolvers are grouped under the `rhai::module_resolvers` module namespace. -| Module Resolver | Description | -| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `FileModuleResolver` | The default module resolution service, not available under [`no_std`]. Loads a script file (based off the current directory) with `.rhai` extension.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | -| `StaticModuleResolver` | Loads modules that are statically added. This can be used under [`no_std`]. | +| Module Resolver | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `FileModuleResolver` | The default module resolution service, not available under [`no_std`] or [WASM] builds. Loads a script file (based off the current directory) with `.rhai` extension.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | +| `StaticModuleResolver` | Loads modules that are statically added. This can be used under [`no_std`]. | An [`Engine`]'s module resolver is set via a call to `Engine::set_module_resolver`: diff --git a/RELEASES.md b/RELEASES.md index fcd99454..4128b19b 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -5,7 +5,7 @@ Version 0.15.1 ============== This is a minor release which enables updating indexers (via registered indexer setters) and supports functions -with `&str` parameters (maps transparently to `ImmutableString`). +with `&str` parameters (maps transparently to `ImmutableString`). WASM is also a tested target. Buf fix ------- @@ -28,7 +28,7 @@ New features * `Engine:register_fn` and `Engine:register_result_fn` accepts functions that take parameters of type `&str` (immutable string slice), which maps directly to `ImmutableString`. This is to avoid needing wrappers for functions taking string parameters. * Set maximum limit on data sizes: `Engine::set_max_string_size`, `Engine::set_max_array_size` and `Engine::set_max_map_size`. * Supports trailing commas on array literals, object map literals, function definitions and function calls. -* Supports compiling to `WASM`. +* Enhances support for compiling to WASM. Version 0.15.0 ============== diff --git a/src/api.rs b/src/api.rs index 4a6e04cc..1ff88316 100644 --- a/src/api.rs +++ b/src/api.rs @@ -555,6 +555,8 @@ impl Engine { /// Read the contents of a file into a string. #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] fn read_file(path: PathBuf) -> Result> { let mut f = File::open(path.clone()).map_err(|err| { Box::new(EvalAltResult::ErrorReadingScriptFile( @@ -598,6 +600,8 @@ impl Engine { /// # } /// ``` #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] pub fn compile_file(&self, path: PathBuf) -> Result> { self.compile_file_with_scope(&Scope::new(), path) } @@ -634,6 +638,8 @@ impl Engine { /// # } /// ``` #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] pub fn compile_file_with_scope( &self, scope: &Scope, @@ -775,6 +781,8 @@ impl Engine { /// # } /// ``` #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] pub fn eval_file(&self, path: PathBuf) -> Result> { Self::read_file(path).and_then(|contents| self.eval::(&contents)) } @@ -799,6 +807,8 @@ impl Engine { /// # } /// ``` #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] pub fn eval_file_with_scope( &self, scope: &mut Scope, @@ -1004,6 +1014,8 @@ impl Engine { /// 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. #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] pub fn consume_file(&self, path: PathBuf) -> Result<(), Box> { Self::read_file(path).and_then(|contents| self.consume(&contents)) } @@ -1011,6 +1023,8 @@ impl Engine { /// Evaluate a file 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. #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] pub fn consume_file_with_scope( &self, scope: &mut Scope, diff --git a/src/engine.rs b/src/engine.rs index 66f04f3b..9c4c65ec 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -292,8 +292,15 @@ impl Default for Engine { #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] module_resolver: Some(Box::new(resolvers::FileModuleResolver::new())), - #[cfg(any(feature = "no_module", feature = "no_std"))] + #[cfg(any( + feature = "no_module", + feature = "no_std", + target_arch = "wasm32", + target_arch = "wasm64" + ))] module_resolver: None, type_names: HashMap::new(), @@ -373,6 +380,8 @@ fn extract_prop_from_setter(fn_name: &str) -> Option<&str> { /// Print/debug to stdout fn default_print(s: &str) { #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] println!("{}", s); } diff --git a/src/lib.rs b/src/lib.rs index d4dedda3..f9c13150 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,6 +37,8 @@ //! engine.register_fn("compute", compute_something); //! //! # #[cfg(not(feature = "no_std"))] +//! # #[cfg(not(target_arch = "wasm32"))] +//! # #[cfg(not(target_arch = "wasm64"))] //! assert_eq!( //! // Evaluate the script, expects a 'bool' return //! engine.eval_file::("my_script.rhai".into())?, diff --git a/src/module.rs b/src/module.rs index d1206b5d..21bf1d11 100644 --- a/src/module.rs +++ b/src/module.rs @@ -1054,6 +1054,8 @@ pub trait ModuleResolver: SendSync { #[cfg(not(feature = "no_module"))] pub mod resolvers { #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] pub use super::file::FileModuleResolver; pub use super::stat::StaticModuleResolver; } @@ -1063,6 +1065,8 @@ pub mod resolvers {} /// Script file-based module resolver. #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] +#[cfg(not(target_arch = "wasm64"))] mod file { use super::*; use crate::stdlib::path::PathBuf; diff --git a/src/packages/mod.rs b/src/packages/mod.rs index 3b89b9f4..6161cd38 100644 --- a/src/packages/mod.rs +++ b/src/packages/mod.rs @@ -33,6 +33,8 @@ pub use pkg_std::StandardPackage; pub use string_basic::BasicStringPackage; pub use string_more::MoreStringPackage; #[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] +#[cfg(not(target_arch = "wasm64"))] pub use time_basic::BasicTimePackage; /// Trait that all packages must implement. diff --git a/src/result.rs b/src/result.rs index 83943df2..cfd96262 100644 --- a/src/result.rs +++ b/src/result.rs @@ -13,6 +13,8 @@ use crate::stdlib::{ }; #[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] +#[cfg(not(target_arch = "wasm64"))] use crate::stdlib::path::PathBuf; /// Evaluation result. @@ -29,6 +31,8 @@ pub enum EvalAltResult { /// /// Never appears under the `no_std` feature. #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] ErrorReadingScriptFile(PathBuf, Position, std::io::Error), /// Call to an unknown function. Wrapped value is the name of the function. @@ -101,7 +105,9 @@ impl EvalAltResult { pub(crate) fn desc(&self) -> &str { match self { #[cfg(not(feature = "no_std"))] - Self::ErrorReadingScriptFile(_, _, _) => "Cannot read from script file", + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] + Self::ErrorReadingScriptFile(_, _, _) => "Cannot read from script file", Self::ErrorParsing(p, _) => p.desc(), Self::ErrorInFunctionCall(_, _, _) => "Error in called function", @@ -160,6 +166,8 @@ impl fmt::Display for EvalAltResult { match self { #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] Self::ErrorReadingScriptFile(path, _, err) => { write!(f, "{} '{}': {}", desc, path.display(), err)? } @@ -259,6 +267,8 @@ impl EvalAltResult { pub fn position(&self) -> Position { match self { #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] Self::ErrorReadingScriptFile(_, pos, _) => *pos, Self::ErrorParsing(_, pos) @@ -297,6 +307,8 @@ impl EvalAltResult { pub fn set_position(&mut self, new_position: Position) { match self { #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm64"))] Self::ErrorReadingScriptFile(_, pos, _) => *pos = new_position, Self::ErrorParsing(_, pos) diff --git a/tests/time.rs b/tests/time.rs index 2590e174..fd5a9182 100644 --- a/tests/time.rs +++ b/tests/time.rs @@ -1,4 +1,6 @@ #![cfg(not(feature = "no_std"))] +#![cfg(not(target_arch = "wasm32"))] +#![cfg(not(target_arch = "wasm64"))] use rhai::{Engine, EvalAltResult, INT}; From 84fbcb03b3b69a99a4e6081b034d4865254afde4 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 17 Jun 2020 15:45:11 +0800 Subject: [PATCH 49/53] Add WASM section. --- README.md | 13 +++++++++---- src/engine.rs | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 34682ab7..cc1411a9 100644 --- a/README.md +++ b/README.md @@ -172,12 +172,17 @@ A _raw_ engine supports, out of the box, only a very [restricted set](#built-in- Selectively include other necessary functionalities by loading specific [packages] to minimize the footprint. Packages are sharable (even across threads via the [`sync`] feature), so they only have to be created once. -### Compiling to WebAssembly (WASM) +### Building to WebAssembly (WASM) -[WASM]: #compiling-to-WebAssembly-wasm +[WASM]: #building-to-WebAssembly-wasm -It is possible to use Rhai when compiling to WebAssembly (WASM), but certain features will not be available, -such as the script file API's and loading modules from external script files. +It is possible to use Rhai when compiling to WebAssembly (WASM). This yields a scripting engine (and language) +that can be run in a standard web browser. Why you would want to is another matter... as there is already +a nice, fast, complete scripting language for the the common WASM environment (i.e. a browser) - and it is called JavaScript. +But anyhow, do it because you _can_! + +When building for WASM, certain features will not be available, such as the script file API's and loading modules +from external script files. Also look into [minimal builds] to reduce generated WASM size. As of this version, a typical, full-featured Rhai scripting engine compiles to a single WASM file around 200KB gzipped. When excluding features that are diff --git a/src/engine.rs b/src/engine.rs index 9c4c65ec..21d055bd 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -6,7 +6,7 @@ use crate::error::ParseErrorType; use crate::fn_native::{CallableFunction, Callback, FnCallArgs}; use crate::module::{resolvers, Module, ModuleResolver}; use crate::optimize::OptimizationLevel; -use crate::packages::{CorePackage, Package, PackageLibrary, PackagesCollection, StandardPackage}; +use crate::packages::{Package, PackageLibrary, PackagesCollection, StandardPackage}; use crate::parser::{Expr, FnAccess, ImmutableString, ReturnType, ScriptFnDef, Stmt, AST, INT}; use crate::r#unsafe::{unsafe_cast_var_name_to_lifetime, unsafe_mut_cast_to_lifetime}; use crate::result::EvalAltResult; From e095a87ea282d568e47311fa3a2e5f6f8299caef Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 17 Jun 2020 16:49:51 +0800 Subject: [PATCH 50/53] Remove wasm64 target. --- src/any.rs | 4 ---- src/api.rs | 8 -------- src/engine.rs | 9 +-------- src/lib.rs | 1 - src/module.rs | 2 -- src/packages/mod.rs | 1 - src/packages/pkg_std.rs | 2 -- src/packages/time_basic.rs | 1 - src/result.rs | 8 +------- tests/time.rs | 1 - 10 files changed, 2 insertions(+), 35 deletions(-) diff --git a/src/any.rs b/src/any.rs index ec25a545..8795fbb9 100644 --- a/src/any.rs +++ b/src/any.rs @@ -25,7 +25,6 @@ use crate::stdlib::{ #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] -#[cfg(not(target_arch = "wasm64"))] use crate::stdlib::time::Instant; /// Trait to represent any type. @@ -195,7 +194,6 @@ impl Dynamic { #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] Union::Variant(value) if value.is::() => "timestamp", Union::Variant(value) => (***value).type_name(), } @@ -220,7 +218,6 @@ impl fmt::Display for Dynamic { #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] Union::Variant(value) if value.is::() => write!(f, ""), Union::Variant(_) => write!(f, "?"), } @@ -245,7 +242,6 @@ impl fmt::Debug for Dynamic { #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] Union::Variant(value) if value.is::() => write!(f, ""), Union::Variant(_) => write!(f, ""), } diff --git a/src/api.rs b/src/api.rs index 1ff88316..ebfb699c 100644 --- a/src/api.rs +++ b/src/api.rs @@ -28,7 +28,6 @@ use crate::stdlib::{ #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] -#[cfg(not(target_arch = "wasm64"))] use crate::stdlib::{fs::File, io::prelude::*, path::PathBuf}; /// Engine public API @@ -556,7 +555,6 @@ impl Engine { /// Read the contents of a file into a string. #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] fn read_file(path: PathBuf) -> Result> { let mut f = File::open(path.clone()).map_err(|err| { Box::new(EvalAltResult::ErrorReadingScriptFile( @@ -601,7 +599,6 @@ impl Engine { /// ``` #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] pub fn compile_file(&self, path: PathBuf) -> Result> { self.compile_file_with_scope(&Scope::new(), path) } @@ -639,7 +636,6 @@ impl Engine { /// ``` #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] pub fn compile_file_with_scope( &self, scope: &Scope, @@ -782,7 +778,6 @@ impl Engine { /// ``` #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] pub fn eval_file(&self, path: PathBuf) -> Result> { Self::read_file(path).and_then(|contents| self.eval::(&contents)) } @@ -808,7 +803,6 @@ impl Engine { /// ``` #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] pub fn eval_file_with_scope( &self, scope: &mut Scope, @@ -1015,7 +1009,6 @@ impl Engine { /// Useful for when you don't need the result, but still need to keep track of possible errors. #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] pub fn consume_file(&self, path: PathBuf) -> Result<(), Box> { Self::read_file(path).and_then(|contents| self.consume(&contents)) } @@ -1024,7 +1017,6 @@ impl Engine { /// Useful for when you don't need the result, but still need to keep track of possible errors. #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] pub fn consume_file_with_scope( &self, scope: &mut Scope, diff --git a/src/engine.rs b/src/engine.rs index 21d055bd..625fb338 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -293,14 +293,8 @@ impl Default for Engine { #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] module_resolver: Some(Box::new(resolvers::FileModuleResolver::new())), - #[cfg(any( - feature = "no_module", - feature = "no_std", - target_arch = "wasm32", - target_arch = "wasm64" - ))] + #[cfg(any(feature = "no_module", feature = "no_std", target_arch = "wasm32",))] module_resolver: None, type_names: HashMap::new(), @@ -381,7 +375,6 @@ fn extract_prop_from_setter(fn_name: &str) -> Option<&str> { fn default_print(s: &str) { #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] println!("{}", s); } diff --git a/src/lib.rs b/src/lib.rs index f9c13150..aeb1470c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,6 @@ //! //! # #[cfg(not(feature = "no_std"))] //! # #[cfg(not(target_arch = "wasm32"))] -//! # #[cfg(not(target_arch = "wasm64"))] //! assert_eq!( //! // Evaluate the script, expects a 'bool' return //! engine.eval_file::("my_script.rhai".into())?, diff --git a/src/module.rs b/src/module.rs index 21bf1d11..8b022dcb 100644 --- a/src/module.rs +++ b/src/module.rs @@ -1055,7 +1055,6 @@ pub trait ModuleResolver: SendSync { pub mod resolvers { #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] pub use super::file::FileModuleResolver; pub use super::stat::StaticModuleResolver; } @@ -1066,7 +1065,6 @@ pub mod resolvers {} #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] -#[cfg(not(target_arch = "wasm64"))] mod file { use super::*; use crate::stdlib::path::PathBuf; diff --git a/src/packages/mod.rs b/src/packages/mod.rs index 6161cd38..d23b9428 100644 --- a/src/packages/mod.rs +++ b/src/packages/mod.rs @@ -34,7 +34,6 @@ pub use string_basic::BasicStringPackage; pub use string_more::MoreStringPackage; #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] -#[cfg(not(target_arch = "wasm64"))] pub use time_basic::BasicTimePackage; /// Trait that all packages must implement. diff --git a/src/packages/pkg_std.rs b/src/packages/pkg_std.rs index 80179f43..dbdeba28 100644 --- a/src/packages/pkg_std.rs +++ b/src/packages/pkg_std.rs @@ -7,7 +7,6 @@ use super::pkg_core::CorePackage; use super::string_more::MoreStringPackage; #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] -#[cfg(not(target_arch = "wasm64"))] use super::time_basic::BasicTimePackage; use crate::def_package; @@ -21,7 +20,6 @@ def_package!(crate:StandardPackage:"_Standard_ package containing all built-in f BasicMapPackage::init(lib); #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] BasicTimePackage::init(lib); MoreStringPackage::init(lib); }); diff --git a/src/packages/time_basic.rs b/src/packages/time_basic.rs index 098c417d..f5e094e0 100644 --- a/src/packages/time_basic.rs +++ b/src/packages/time_basic.rs @@ -1,5 +1,4 @@ #![cfg(not(target_arch = "wasm32"))] -#![cfg(not(target_arch = "wasm64"))] use super::logic::{eq, gt, gte, lt, lte, ne}; use super::math_basic::MAX_INT; diff --git a/src/result.rs b/src/result.rs index cfd96262..760a9a5e 100644 --- a/src/result.rs +++ b/src/result.rs @@ -14,7 +14,6 @@ use crate::stdlib::{ #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] -#[cfg(not(target_arch = "wasm64"))] use crate::stdlib::path::PathBuf; /// Evaluation result. @@ -32,7 +31,6 @@ pub enum EvalAltResult { /// Never appears under the `no_std` feature. #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] ErrorReadingScriptFile(PathBuf, Position, std::io::Error), /// Call to an unknown function. Wrapped value is the name of the function. @@ -106,8 +104,7 @@ impl EvalAltResult { match self { #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] - Self::ErrorReadingScriptFile(_, _, _) => "Cannot read from script file", + Self::ErrorReadingScriptFile(_, _, _) => "Cannot read from script file", Self::ErrorParsing(p, _) => p.desc(), Self::ErrorInFunctionCall(_, _, _) => "Error in called function", @@ -167,7 +164,6 @@ impl fmt::Display for EvalAltResult { match self { #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] Self::ErrorReadingScriptFile(path, _, err) => { write!(f, "{} '{}': {}", desc, path.display(), err)? } @@ -268,7 +264,6 @@ impl EvalAltResult { match self { #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] Self::ErrorReadingScriptFile(_, pos, _) => *pos, Self::ErrorParsing(_, pos) @@ -308,7 +303,6 @@ impl EvalAltResult { match self { #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - #[cfg(not(target_arch = "wasm64"))] Self::ErrorReadingScriptFile(_, pos, _) => *pos = new_position, Self::ErrorParsing(_, pos) diff --git a/tests/time.rs b/tests/time.rs index fd5a9182..9873decf 100644 --- a/tests/time.rs +++ b/tests/time.rs @@ -1,6 +1,5 @@ #![cfg(not(feature = "no_std"))] #![cfg(not(target_arch = "wasm32"))] -#![cfg(not(target_arch = "wasm64"))] use rhai::{Engine, EvalAltResult, INT}; From ae6d5e13a9fc5e877fa1d3287aab79aba30222ae Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 17 Jun 2020 16:50:46 +0800 Subject: [PATCH 51/53] Gate i128/u128 for wasm32 target. --- src/packages/arithmetic.rs | 85 +++++++++++++++++++++++++++--------- src/packages/array_basic.rs | 13 ++++-- src/packages/iter_basic.rs | 10 ++++- src/packages/logic.rs | 22 +++++++--- src/packages/math_basic.rs | 8 +++- src/packages/string_basic.rs | 13 ++++-- src/packages/string_more.rs | 10 ++++- src/stdlib.rs | 7 ++- 8 files changed, 127 insertions(+), 41 deletions(-) diff --git a/src/packages/arithmetic.rs b/src/packages/arithmetic.rs index ead54f8c..cf109b0c 100644 --- a/src/packages/arithmetic.rs +++ b/src/packages/arithmetic.rs @@ -275,27 +275,51 @@ def_package!(crate:ArithmeticPackage:"Basic arithmetic", lib, { #[cfg(not(feature = "unchecked"))] { // Checked basic arithmetic - reg_op!(lib, "+", add, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "-", sub, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "*", mul, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "/", div, i8, u8, i16, u16, i32, u32, u64, i128, u128); + reg_op!(lib, "+", add, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "-", sub, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "*", mul, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "/", div, i8, u8, i16, u16, i32, u32, u64); // Checked bit shifts - reg_op!(lib, "<<", shl, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, ">>", shr, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "%", modulo, i8, u8, i16, u16, i32, u32, u64, i128, u128); + reg_op!(lib, "<<", shl, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, ">>", shr, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "%", modulo, i8, u8, i16, u16, i32, u32, u64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_op!(lib, "+", add, i128, u128); + reg_op!(lib, "-", sub, i128, u128); + reg_op!(lib, "*", mul, i128, u128); + reg_op!(lib, "/", div, i128, u128); + // Checked bit shifts + reg_op!(lib, "<<", shl, i128, u128); + reg_op!(lib, ">>", shr, i128, u128); + reg_op!(lib, "%", modulo, i128, u128); + } } #[cfg(feature = "unchecked")] { // Unchecked basic arithmetic - reg_op!(lib, "+", add_u, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "-", sub_u, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "*", mul_u, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "/", div_u, i8, u8, i16, u16, i32, u32, u64, i128, u128); + reg_op!(lib, "+", add_u, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "-", sub_u, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "*", mul_u, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "/", div_u, i8, u8, i16, u16, i32, u32, u64); // Unchecked bit shifts - reg_op!(lib, "<<", shl_u, i64, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, ">>", shr_u, i64, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "%", modulo_u, i8, u8, i16, u16, i32, u32, u64, i128, u128); + reg_op!(lib, "<<", shl_u, i64, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, ">>", shr_u, i64, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "%", modulo_u, i8, u8, i16, u16, i32, u32, u64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_op!(lib, "+", add_u, i128, u128); + reg_op!(lib, "-", sub_u, i128, u128); + reg_op!(lib, "*", mul_u, i128, u128); + reg_op!(lib, "/", div_u, i128, u128); + // Unchecked bit shifts + reg_op!(lib, "<<", shl_u, i128, u128); + reg_op!(lib, ">>", shr_u, i128, u128); + reg_op!(lib, "%", modulo_u, i128, u128); + } } } @@ -311,9 +335,16 @@ def_package!(crate:ArithmeticPackage:"Basic arithmetic", lib, { #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] { - reg_op!(lib, "|", binary_or, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "&", binary_and, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "^", binary_xor, i8, u8, i16, u16, i32, u32, u64, i128, u128); + reg_op!(lib, "|", binary_or, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "&", binary_and, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "^", binary_xor, i8, u8, i16, u16, i32, u32, u64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_op!(lib, "|", binary_or, i128, u128); + reg_op!(lib, "&", binary_and, i128, u128); + reg_op!(lib, "^", binary_xor, i128, u128); + } } #[cfg(not(feature = "no_float"))] @@ -343,8 +374,14 @@ def_package!(crate:ArithmeticPackage:"Basic arithmetic", lib, { #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] { - reg_unary!(lib, "-", neg, i8, i16, i32, i64, i128); - reg_unary!(lib, "abs", abs, i8, i16, i32, i64, i128); + reg_unary!(lib, "-", neg, i8, i16, i32, i64); + reg_unary!(lib, "abs", abs, i8, i16, i32, i64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_unary!(lib, "-", neg, i128); + reg_unary!(lib, "abs", abs, i128); + } } } @@ -357,8 +394,14 @@ def_package!(crate:ArithmeticPackage:"Basic arithmetic", lib, { #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] { - reg_unary!(lib, "-", neg_u, i8, i16, i32, i64, i128); - reg_unary!(lib, "abs", abs_u, i8, i16, i32, i64, i128); + reg_unary!(lib, "-", neg_u, i8, i16, i32, i64); + reg_unary!(lib, "abs", abs_u, i8, i16, i32, i64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_unary!(lib, "-", neg_u, i128); + reg_unary!(lib, "abs", abs_u, i128); + } } } }); diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index c6904e90..59358063 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -98,9 +98,16 @@ def_package!(crate:BasicArrayPackage:"Basic array utilities.", lib, { #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] { - reg_op!(lib, "push", push, i8, u8, i16, u16, i32, i64, u32, u64, i128, u128); - reg_pad!(lib, "pad", pad, i8, u8, i16, u16, i32, u32, i64, u64, i128, u128); - reg_tri!(lib, "insert", ins, i8, u8, i16, u16, i32, i64, u32, u64, i128, u128); + reg_op!(lib, "push", push, i8, u8, i16, u16, i32, i64, u32, u64); + reg_pad!(lib, "pad", pad, i8, u8, i16, u16, i32, u32, i64, u64); + reg_tri!(lib, "insert", ins, i8, u8, i16, u16, i32, i64, u32, u64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_op!(lib, "push", push, i128, u128); + reg_pad!(lib, "pad", pad, i128, u128); + reg_tri!(lib, "insert", ins, i128, u128); + } } #[cfg(not(feature = "no_float"))] diff --git a/src/packages/iter_basic.rs b/src/packages/iter_basic.rs index ebf55992..cccf4b6c 100644 --- a/src/packages/iter_basic.rs +++ b/src/packages/iter_basic.rs @@ -85,7 +85,10 @@ def_package!(crate:BasicIteratorPackage:"Basic range iterators.", lib, { ) } - reg_range!(lib, "range", i8, u8, i16, u16, i32, i64, u32, u64, i128, u128); + reg_range!(lib, "range", i8, u8, i16, u16, i32, i64, u32, u64); + + #[cfg(not(target_arch = "wasm32"))] + reg_range!(lib, "range", i128, u128); } reg_step::(lib); @@ -103,6 +106,9 @@ def_package!(crate:BasicIteratorPackage:"Basic range iterators.", lib, { ) } - reg_step!(lib, "range", i8, u8, i16, u16, i32, i64, u32, u64, i128, u128); + reg_step!(lib, "range", i8, u8, i16, u16, i32, i64, u32, u64); + + #[cfg(not(target_arch = "wasm32"))] + reg_step!(lib, "range", i128, u128); } }); diff --git a/src/packages/logic.rs b/src/packages/logic.rs index 41ca035f..1cb4b437 100644 --- a/src/packages/logic.rs +++ b/src/packages/logic.rs @@ -36,12 +36,22 @@ def_package!(crate:LogicPackage:"Logical operators.", lib, { #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] { - reg_op!(lib, "<", lt, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "<=", lte, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, ">", gt, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, ">=", gte, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "==", eq, i8, u8, i16, u16, i32, u32, u64, i128, u128); - reg_op!(lib, "!=", ne, i8, u8, i16, u16, i32, u32, u64, i128, u128); + reg_op!(lib, "<", lt, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "<=", lte, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, ">", gt, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, ">=", gte, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "==", eq, i8, u8, i16, u16, i32, u32, u64); + reg_op!(lib, "!=", ne, i8, u8, i16, u16, i32, u32, u64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_op!(lib, "<", lt, i128, u128); + reg_op!(lib, "<=", lte, i128, u128); + reg_op!(lib, ">", gt, i128, u128); + reg_op!(lib, ">=", gte, i128, u128); + reg_op!(lib, "==", eq, i128, u128); + reg_op!(lib, "!=", ne, i128, u128); + } } #[cfg(not(feature = "no_float"))] diff --git a/src/packages/math_basic.rs b/src/packages/math_basic.rs index eea49727..e7ce43b7 100644 --- a/src/packages/math_basic.rs +++ b/src/packages/math_basic.rs @@ -70,8 +70,12 @@ def_package!(crate:BasicMathPackage:"Basic mathematic functions.", lib, { lib.set_fn_1("to_float", |x: u32| Ok(x as FLOAT)); lib.set_fn_1("to_float", |x: i64| Ok(x as FLOAT)); lib.set_fn_1("to_float", |x: u64| Ok(x as FLOAT)); - lib.set_fn_1("to_float", |x: i128| Ok(x as FLOAT)); - lib.set_fn_1("to_float", |x: u128| Ok(x as FLOAT)); + + #[cfg(not(target_arch = "wasm32"))] + { + lib.set_fn_1("to_float", |x: i128| Ok(x as FLOAT)); + lib.set_fn_1("to_float", |x: u128| Ok(x as FLOAT)); + } } } diff --git a/src/packages/string_basic.rs b/src/packages/string_basic.rs index 262a4f3a..338cf5ec 100644 --- a/src/packages/string_basic.rs +++ b/src/packages/string_basic.rs @@ -52,9 +52,16 @@ def_package!(crate:BasicStringPackage:"Basic string utilities, including printin reg_op!(lib, KEYWORD_PRINT, to_string, i8, u8, i16, u16, i32, u32); reg_op!(lib, FUNC_TO_STRING, to_string, i8, u8, i16, u16, i32, u32); reg_op!(lib, KEYWORD_DEBUG, to_debug, i8, u8, i16, u16, i32, u32); - reg_op!(lib, KEYWORD_PRINT, to_string, i64, u64, i128, u128); - reg_op!(lib, FUNC_TO_STRING, to_string, i64, u64, i128, u128); - reg_op!(lib, KEYWORD_DEBUG, to_debug, i64, u64, i128, u128); + reg_op!(lib, KEYWORD_PRINT, to_string, i64, u64); + reg_op!(lib, FUNC_TO_STRING, to_string, i64, u64); + reg_op!(lib, KEYWORD_DEBUG, to_debug, i64, u64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_op!(lib, KEYWORD_PRINT, to_string, i128, u128); + reg_op!(lib, FUNC_TO_STRING, to_string, i128, u128); + reg_op!(lib, KEYWORD_DEBUG, to_debug, i128, u128); + } } #[cfg(not(feature = "no_float"))] diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 8f8d1731..46805750 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -95,8 +95,14 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] { - reg_op!(lib, "+", append, i8, u8, i16, u16, i32, i64, u32, u64, i128, u128); - reg_op!(lib, "+", prepend, i8, u8, i16, u16, i32, i64, u32, u64, i128, u128); + reg_op!(lib, "+", append, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op!(lib, "+", prepend, i8, u8, i16, u16, i32, i64, u32, u64); + + #[cfg(not(target_arch = "wasm32"))] + { + reg_op!(lib, "+", append, i128, u128); + reg_op!(lib, "+", prepend, i128, u128); + } } #[cfg(not(feature = "no_float"))] diff --git a/src/stdlib.rs b/src/stdlib.rs index 1d1397d5..64eda6fb 100644 --- a/src/stdlib.rs +++ b/src/stdlib.rs @@ -4,10 +4,13 @@ mod inner { pub use core::{ any, arch, array, ascii, cell, char, clone, cmp, convert, default, f32, f64, ffi, fmt, - future, hash, hint, i128, i16, i32, i64, i8, isize, iter, marker, mem, num, ops, option, - panic, pin, prelude, ptr, result, slice, str, task, time, u128, u16, u32, u64, u8, usize, + future, hash, hint, i16, i32, i64, i8, isize, iter, marker, mem, num, ops, option, panic, + pin, prelude, ptr, result, slice, str, task, time, u16, u32, u64, u8, usize, }; + #[cfg(not(target_arch = "wasm32"))] + pub use core::{i128, u128}; + pub use alloc::{borrow, boxed, format, rc, string, sync, vec}; pub use core_error as error; From 2a738415492928de4f1527f6264425576c9da5d4 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 17 Jun 2020 16:50:57 +0800 Subject: [PATCH 52/53] Add support for Instant for wasm32. --- Cargo.toml | 3 +++ src/any.rs | 4 ++++ src/packages/mod.rs | 1 - src/packages/pkg_std.rs | 2 -- src/packages/time_basic.rs | 9 +++++---- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c1ea251a..6ced850a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,3 +63,6 @@ version = "0.3.2" default-features = false features = ["compile-time-rng"] optional = true + +[target.'cfg(target_arch = "wasm32")'.dependencies] +instant= "0.1.4" # WASM implementation of std::time::Instant diff --git a/src/any.rs b/src/any.rs index 8795fbb9..6a5d9050 100644 --- a/src/any.rs +++ b/src/any.rs @@ -27,6 +27,10 @@ use crate::stdlib::{ #[cfg(not(target_arch = "wasm32"))] use crate::stdlib::time::Instant; +#[cfg(not(feature = "no_std"))] +#[cfg(target_arch = "wasm32")] +use instant::Instant; + /// Trait to represent any type. /// /// Currently, `Variant` is not `Send` nor `Sync`, so it can practically be any type. diff --git a/src/packages/mod.rs b/src/packages/mod.rs index d23b9428..3b89b9f4 100644 --- a/src/packages/mod.rs +++ b/src/packages/mod.rs @@ -33,7 +33,6 @@ pub use pkg_std::StandardPackage; pub use string_basic::BasicStringPackage; pub use string_more::MoreStringPackage; #[cfg(not(feature = "no_std"))] -#[cfg(not(target_arch = "wasm32"))] pub use time_basic::BasicTimePackage; /// Trait that all packages must implement. diff --git a/src/packages/pkg_std.rs b/src/packages/pkg_std.rs index dbdeba28..d2790d50 100644 --- a/src/packages/pkg_std.rs +++ b/src/packages/pkg_std.rs @@ -6,7 +6,6 @@ use super::math_basic::BasicMathPackage; use super::pkg_core::CorePackage; use super::string_more::MoreStringPackage; #[cfg(not(feature = "no_std"))] -#[cfg(not(target_arch = "wasm32"))] use super::time_basic::BasicTimePackage; use crate::def_package; @@ -19,7 +18,6 @@ def_package!(crate:StandardPackage:"_Standard_ package containing all built-in f #[cfg(not(feature = "no_object"))] BasicMapPackage::init(lib); #[cfg(not(feature = "no_std"))] - #[cfg(not(target_arch = "wasm32"))] BasicTimePackage::init(lib); MoreStringPackage::init(lib); }); diff --git a/src/packages/time_basic.rs b/src/packages/time_basic.rs index f5e094e0..9fd4e21f 100644 --- a/src/packages/time_basic.rs +++ b/src/packages/time_basic.rs @@ -1,5 +1,4 @@ -#![cfg(not(target_arch = "wasm32"))] - +#![cfg(not(feature = "no_std"))] use super::logic::{eq, gt, gte, lt, lte, ne}; use super::math_basic::MAX_INT; @@ -9,13 +8,15 @@ use crate::parser::INT; use crate::result::EvalAltResult; use crate::token::Position; -#[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] use crate::stdlib::time::Instant; +#[cfg(target_arch = "wasm32")] +use instant::Instant; + #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; -#[cfg(not(feature = "no_std"))] def_package!(crate:BasicTimePackage:"Basic timing utilities.", lib, { // Register date/time functions lib.set_fn_0("timestamp", || Ok(Instant::now())); From 2f815e277d8cb6e603a29551f03bc59982672f21 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 18 Jun 2020 09:37:44 +0800 Subject: [PATCH 53/53] Add wasm-bindgen to instant crate for wasm. --- Cargo.toml | 2 +- README.md | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6ced850a..1f38d142 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,4 +65,4 @@ features = ["compile-time-rng"] optional = true [target.'cfg(target_arch = "wasm32")'.dependencies] -instant= "0.1.4" # WASM implementation of std::time::Instant +instant= { version = "0.1.4", features = ["wasm-bindgen"] } # WASM implementation of std::time::Instant diff --git a/README.md b/README.md index cc1411a9..d79c1218 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,9 @@ to add scripting to any application. Supported targets ----------------- -* All common targets, including [WASM] and `no-std`. +* All common CPU targets for Windows, Linux and MacOS. +* [WASM] +* `no-std` Features -------- @@ -185,9 +187,11 @@ When building for WASM, certain features will not be available, such as the scri from external script files. Also look into [minimal builds] to reduce generated WASM size. As of this version, a typical, full-featured -Rhai scripting engine compiles to a single WASM file around 200KB gzipped. When excluding features that are +Rhai scripting engine compiles to a single WASM file less than 200KB gzipped. When excluding features that are marginal in WASM environment, the gzipped payload can be further shrunk to 160KB. +In benchmark tests, a WASM build runs scripts roughly 1.7-2.2x slower than a native optimized release build. + Related Resources -----------------