diff --git a/README.md b/README.md index 36337932..7122feb6 100644 --- a/README.md +++ b/README.md @@ -1977,8 +1977,8 @@ When embedding Rhai into an application, it is usually necessary to trap `print` (for logging into a tracking log, for example). ```rust -// Any function or closure that takes an &str argument can be used to override -// print and debug +// Any function or closure that takes an '&str' argument can be used to override +// 'print' and 'debug' engine.on_print(|x| println!("hello: {}", x)); engine.on_debug(|x| println!("DEBUG: {}", x)); @@ -2007,14 +2007,36 @@ Using external modules [module]: #using-external-modules [modules]: #using-external-modules +Rhai allows organizing code (functions and variables) into _modules_. A module is a single script file +with `export` statements that _exports_ certain global variables and functions as contents of the module. + +Everything exported as part of a module is constant and read-only. + +A module can be _imported_ via the `import` statement, and its members accessed via '`::`' similar to C++. + ```rust -import "crypto" as crypto; // Import an external script file as a module +import "crypto" as crypto; // import the script file 'crypto.rhai' as a module -crypto::encrypt(secret); // Use functions defined under the module via '::' +crypto::encrypt(secret); // use functions defined under the module via '::' -print(crypto::status); // Module variables are constants +print(crypto::status); // module variables are constants -crypto::hash::sha256(key); // Sub-modules are also supported +crypto::hash::sha256(key); // sub-modules are also supported +``` + +`import` statements are _scoped_, meaning that they are only accessible inside the scope that they're imported. + +```rust +let mod = "crypto"; + +if secured { // new block scope + import mod as crypto; // import module (the path needs not be a constant string) + + crypto::encrypt(key); // use a function in the module +} // the module disappears at the end of the block scope + +crypto::encrypt(others); // <- this causes a run-time error because the 'crypto' module + // is no longer available! ``` Script optimization diff --git a/src/any.rs b/src/any.rs index 28c462d1..e07d584e 100644 --- a/src/any.rs +++ b/src/any.rs @@ -1,6 +1,6 @@ //! Helper module which defines the `Any` trait to to allow dynamic value handling. -use crate::engine::{Array, Map}; +use crate::engine::{Array, Map, SubScope}; use crate::parser::INT; #[cfg(not(feature = "no_float"))] @@ -135,6 +135,7 @@ pub enum Union { Float(FLOAT), Array(Box), Map(Box), + SubScope(Box), Variant(Box>), } @@ -165,6 +166,7 @@ impl Dynamic { Union::Float(_) => TypeId::of::(), Union::Array(_) => TypeId::of::(), Union::Map(_) => TypeId::of::(), + Union::SubScope(_) => TypeId::of::(), Union::Variant(value) => (***value).type_id(), } } @@ -181,6 +183,7 @@ impl Dynamic { Union::Float(_) => type_name::(), Union::Array(_) => "array", Union::Map(_) => "map", + Union::SubScope(_) => "sub-scope", #[cfg(not(feature = "no_std"))] Union::Variant(value) if value.is::() => "timestamp", @@ -201,6 +204,7 @@ impl fmt::Display for Dynamic { Union::Float(value) => write!(f, "{}", value), Union::Array(value) => write!(f, "{:?}", value), Union::Map(value) => write!(f, "#{:?}", value), + Union::SubScope(value) => write!(f, "#{:?}", value), Union::Variant(_) => write!(f, "?"), } } @@ -218,6 +222,7 @@ impl fmt::Debug for Dynamic { Union::Float(value) => write!(f, "{:?}", value), Union::Array(value) => write!(f, "{:?}", value), Union::Map(value) => write!(f, "#{:?}", value), + Union::SubScope(value) => write!(f, "#{:?}", value), Union::Variant(_) => write!(f, ""), } } @@ -235,6 +240,7 @@ impl Clone for Dynamic { Union::Float(value) => Self(Union::Float(value)), Union::Array(ref value) => Self(Union::Array(value.clone())), Union::Map(ref value) => Self(Union::Map(value.clone())), + Union::SubScope(ref value) => Self(Union::SubScope(value.clone())), Union::Variant(ref value) => (***value).clone_into_dynamic(), } } @@ -363,6 +369,7 @@ impl Dynamic { Union::Float(ref value) => (value as &dyn Any).downcast_ref::().cloned(), Union::Array(value) => cast_box::<_, T>(value).ok(), Union::Map(value) => cast_box::<_, T>(value).ok(), + Union::SubScope(value) => cast_box::<_, T>(value).ok(), Union::Variant(value) => value.as_any().downcast_ref::().cloned(), } } @@ -400,6 +407,7 @@ impl Dynamic { Union::Float(ref value) => (value as &dyn Any).downcast_ref::().unwrap().clone(), Union::Array(value) => cast_box::<_, T>(value).unwrap(), Union::Map(value) => cast_box::<_, T>(value).unwrap(), + Union::SubScope(value) => cast_box::<_, T>(value).unwrap(), Union::Variant(value) => value.as_any().downcast_ref::().unwrap().clone(), } } @@ -422,6 +430,7 @@ impl Dynamic { Union::Float(value) => (value as &dyn Any).downcast_ref::(), Union::Array(value) => (value.as_ref() as &dyn Any).downcast_ref::(), Union::Map(value) => (value.as_ref() as &dyn Any).downcast_ref::(), + Union::SubScope(value) => (value.as_ref() as &dyn Any).downcast_ref::(), Union::Variant(value) => value.as_ref().as_ref().as_any().downcast_ref::(), } } @@ -444,6 +453,7 @@ impl Dynamic { Union::Float(value) => (value as &mut dyn Any).downcast_mut::(), Union::Array(value) => (value.as_mut() as &mut dyn Any).downcast_mut::(), Union::Map(value) => (value.as_mut() as &mut dyn Any).downcast_mut::(), + Union::SubScope(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 8ec80117..884e757b 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -18,6 +18,7 @@ use crate::stdlib::{ hash::{Hash, Hasher}, iter::once, mem, + num::NonZeroUsize, ops::{Deref, DerefMut}, rc::Rc, string::{String, ToString}, @@ -41,6 +42,32 @@ pub type Array = Vec; /// Not available under the `no_object` feature. pub type Map = HashMap; +/// A sub-scope - basically an imported module namespace. +/// +/// Not available under the `no_import` feature. +#[derive(Debug, Clone)] +pub struct SubScope(HashMap); + +impl SubScope { + /// Create a new sub-scope. + pub fn new() -> Self { + Self(HashMap::new()) + } +} + +impl Deref for SubScope { + type Target = HashMap; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SubScope { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + pub type FnCallArgs<'a> = [&'a mut Dynamic]; #[cfg(feature = "sync")] @@ -472,39 +499,68 @@ fn default_print(s: &str) { println!("{}", s); } -/// Search for a variable within the scope, returning its value and index inside the Scope -fn search_scope<'a>( +/// Search for a variable within the scope +fn search_scope_variables<'a>( scope: &'a mut Scope, name: &str, - modules: Option<&Box>>, + index: Option, pos: Position, ) -> Result<(&'a mut Dynamic, ScopeEntryType), Box> { - if let Some(modules) = modules { - let (id, root_pos) = modules.get(0); // First module - let mut sub_scope = scope - .find_sub_scope(id) - .ok_or_else(|| Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)))?; - - for x in 1..modules.len() { - let (id, id_pos) = modules.get(x); - - sub_scope = sub_scope - .get_mut(id) - .unwrap() - .downcast_mut::() - .ok_or_else(|| Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *id_pos)))?; - } - - sub_scope - .get_mut(name) - .map(|v| (v, ScopeEntryType::Constant)) - .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.into(), pos))) + let index = if let Some(index) = index { + scope.len() - index.get() } else { - let (index, _) = scope + scope .get_index(name) - .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.into(), pos)))?; + .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.into(), pos)))? + .0 + }; - Ok(scope.get_mut(index)) + Ok(scope.get_mut(index)) +} + +/// Search for a sub-scope within the scope +fn search_scope_modules<'a>( + scope: &'a mut Scope, + name: &str, + modules: &Box>, + index: Option, + pos: Position, +) -> Result<(&'a mut Dynamic, ScopeEntryType), Box> { + let (id, root_pos) = modules.get(0); // First module + + let mut sub_scope = if let Some(index) = index { + scope + .get_mut(scope.len() - index.get()) + .0 + .downcast_mut::() + .unwrap() + } else { + scope + .find_sub_scope(id) + .ok_or_else(|| Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)))? + }; + + for x in 1..modules.len() { + let (id, id_pos) = modules.get(x); + + sub_scope = sub_scope + .get_mut(id) + .and_then(|v| v.downcast_mut::()) + .ok_or_else(|| Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *id_pos)))?; + } + + let result = sub_scope + .get_mut(name) + .map(|v| (v, ScopeEntryType::Constant)) + .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.into(), pos)))?; + + if result.0.is::() { + Err(Box::new(EvalAltResult::ErrorVariableNotFound( + name.into(), + pos, + ))) + } else { + Ok(result) } } @@ -973,20 +1029,24 @@ impl Engine { match dot_lhs { // id.??? or id[???] Expr::Variable(id, modules, index, pos) => { - let (target, typ) = match index { - Some(i) if !state.always_search => scope.get_mut(scope.len() - i.get()), - _ => search_scope(scope, id, modules.as_ref(), *pos)?, + let index = if state.always_search { None } else { *index }; + + let (target, typ) = if let Some(modules) = modules { + search_scope_modules(scope, id, modules, index, *pos)? + } else { + search_scope_variables(scope, id, index, *pos)? }; // Constants cannot be modified match typ { + ScopeEntryType::SubScope => unreachable!(), ScopeEntryType::Constant if new_val.is_some() => { return Err(Box::new(EvalAltResult::ErrorAssignmentToConstant( id.to_string(), *pos, ))); } - _ => (), + ScopeEntryType::Constant | ScopeEntryType::Normal => (), } let this_ptr = target.into(); @@ -1206,11 +1266,16 @@ impl Engine { Expr::FloatConstant(f, _) => Ok((*f).into()), Expr::StringConstant(s, _) => Ok(s.to_string().into()), Expr::CharConstant(c, _) => Ok((*c).into()), - Expr::Variable(_, None, Some(index), _) if !state.always_search => { - Ok(scope.get_mut(scope.len() - index.get()).0.clone()) - } - Expr::Variable(id, modules, _, pos) => { - search_scope(scope, id, modules.as_ref(), *pos).map(|(v, _)| v.clone()) + Expr::Variable(id, modules, index, pos) => { + let index = if state.always_search { None } else { *index }; + + let val = if let Some(modules) = modules { + search_scope_modules(scope, id, modules, index, *pos)? + } else { + search_scope_variables(scope, id, index, *pos)? + }; + + Ok(val.0.clone()) } Expr::Property(_, _) => unreachable!(), @@ -1223,8 +1288,15 @@ impl Engine { match lhs.as_ref() { // name = rhs - Expr::Variable(name, modules, _, pos) => { - match search_scope(scope, name, modules.as_ref(), *pos)? { + Expr::Variable(name, modules, index, pos) => { + let index = if state.always_search { None } else { *index }; + let val = if let Some(modules) = modules { + search_scope_modules(scope, name, modules, index, *pos)? + } else { + search_scope_variables(scope, name, index, *pos)? + }; + + match val { (_, ScopeEntryType::Constant) => Err(Box::new( EvalAltResult::ErrorAssignmentToConstant(name.to_string(), *op_pos), )), @@ -1562,7 +1634,7 @@ impl Engine { .eval_expr(scope, state, fn_lib, expr, level)? .try_cast::() { - let mut module = Map::new(); + let mut module = SubScope::new(); module.insert("kitty".to_string(), "foo".to_string().into()); module.insert("path".to_string(), path.into()); diff --git a/src/lib.rs b/src/lib.rs index e13eaf0f..beeafb46 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,6 +103,9 @@ pub use engine::Array; #[cfg(not(feature = "no_object"))] pub use engine::Map; +#[cfg(not(feature = "no_import"))] +pub use engine::SubScope; + #[cfg(not(feature = "no_float"))] pub use parser::FLOAT; diff --git a/src/parser.rs b/src/parser.rs index 7dcb4d65..8f242e0f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1070,9 +1070,9 @@ fn parse_primary<'a>( (Expr::Property(id, pos), Token::LeftParen) => { parse_call_expr(input, stack, id, None, pos, allow_stmt_expr)? } - // moduled + // module access #[cfg(not(feature = "no_import"))] - (Expr::Variable(id, mut modules, _, pos), Token::DoubleColon) => { + (Expr::Variable(id, mut modules, mut index, pos), Token::DoubleColon) => { match input.next().unwrap() { (Token::Identifier(id2), pos2) => { if let Some(ref mut modules) = modules { @@ -1081,10 +1081,11 @@ fn parse_primary<'a>( let mut vec = StaticVec::new(); vec.push((*id, pos)); modules = Some(Box::new(vec)); + + let root = modules.as_ref().unwrap().get(0); + index = stack.find_sub_scope(&root.0); } - let root = modules.as_ref().unwrap().get(0); - let index = stack.find_sub_scope(&root.0); Expr::Variable(Box::new(id2), modules, index, pos2) } (_, pos2) => return Err(PERR::VariableExpected.into_err(pos2)), @@ -1828,6 +1829,7 @@ fn parse_import<'a>( (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; + stack.push((name.clone(), ScopeEntryType::SubScope)); Ok(Stmt::Import(Box::new(expr), Box::new(name), pos)) } diff --git a/src/scope.rs b/src/scope.rs index 3f55aeaa..a965930f 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,7 +1,7 @@ //! Module that defines the `Scope` type representing a function call-stack scope. -use crate::any::{Dynamic, Variant}; -use crate::engine::Map; +use crate::any::{Dynamic, Union, Variant}; +use crate::engine::SubScope; use crate::parser::{map_dynamic_to_expr, Expr}; use crate::token::Position; @@ -175,11 +175,11 @@ impl<'a> Scope<'a> { /// # Examples /// /// ``` - /// use rhai::{Scope, Map}; + /// use rhai::{Scope, SubScope}; /// /// let mut my_scope = Scope::new(); /// - /// let mut sub_scope = Map::new(); + /// let mut sub_scope = SubScope::new(); /// sub_scope.insert("x".to_string(), 42_i64.into()); /// /// my_scope.push_sub_scope("my_plugin", sub_scope); @@ -187,8 +187,13 @@ impl<'a> Scope<'a> { /// let s = my_scope.find_sub_scope("my_plugin").unwrap(); /// assert_eq!(*s.get("x").unwrap().downcast_ref::().unwrap(), 42); /// ``` - pub fn push_sub_scope>>(&mut self, name: K, value: Map) { - self.push_dynamic_value(name, EntryType::SubScope, value.into(), true); + pub fn push_sub_scope>>(&mut self, name: K, value: SubScope) { + self.push_dynamic_value( + name, + EntryType::SubScope, + Dynamic(Union::SubScope(Box::new(value))), + true, + ); } /// Add (push) a new constant to the Scope. @@ -352,9 +357,9 @@ impl<'a> Scope<'a> { } /// Find a sub-scope in the Scope, starting from the last entry. - pub fn find_sub_scope(&mut self, name: &str) -> Option<&mut Map> { + pub fn find_sub_scope(&mut self, name: &str) -> Option<&mut SubScope> { let index = self.get_sub_scope_index(name)?; - self.get_mut(index).0.downcast_mut() + self.get_mut(index).0.downcast_mut::() } /// Get the value of an entry in the Scope, starting from the last. diff --git a/tests/modules.rs b/tests/modules.rs new file mode 100644 index 00000000..9052edb3 --- /dev/null +++ b/tests/modules.rs @@ -0,0 +1,15 @@ +use rhai::{EvalAltResult, Scope, SubScope, INT}; + +#[test] +#[cfg(not(feature = "no_import"))] +fn test_sub_scope() { + let mut my_scope = Scope::new(); + + let mut sub_scope = SubScope::new(); + sub_scope.insert("x".to_string(), (42 as INT).into()); + + my_scope.push_sub_scope("my_plugin", sub_scope); + + let s = my_scope.find_sub_scope("my_plugin").unwrap(); + assert_eq!(*s.get("x").unwrap().downcast_ref::().unwrap(), 42); +}