diff --git a/README.md b/README.md index 95c0d274..bb5f3597 100644 --- a/README.md +++ b/README.md @@ -2044,10 +2044,30 @@ 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. +Rhai allows organizing code (functions and variables) into _modules_. +Modules can be disabled via the [`no_module`] feature. -Everything exported as part of a module is constant and read-only. +### Exporting variables and functions + +A module is a single script (or pre-compiled `AST`) containing global variables and functions. +The `export` statement, which can only be at global level, exposes selected variables as members of a module. +Variables not exported are private and invisible to the outside. + +All functions are automatically exported. Everything exported from a module is **constant** (**read-only**). + +```rust +// This is a module script. + +fn inc(x) { x + 1 } // function + +let private = 123; // variable not exported - invisible to outside +let x = 42; // this will be exported below + +export x; // the variable 'x' is exported under its own name + +export x as answer; // the variable 'x' is exported under the alias 'answer' + // another script can load this module and access 'x' as 'module::answer' +``` ### Importing modules diff --git a/src/engine.rs b/src/engine.rs index 0029684d..d026ecb0 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1622,7 +1622,10 @@ impl Engine { .try_cast::() { if let Some(resolver) = self.module_resolver.as_ref() { - let module = resolver.resolve(self, &path, expr.position())?; + // Use an empty scope to create a module + let mut mod_scope = Scope::new(); + let module = + resolver.resolve(self, mod_scope, &path, expr.position())?; // TODO - avoid copying module name in inner block? let mod_name = name.as_ref().clone(); @@ -1639,6 +1642,36 @@ impl Engine { } } } + + // Export statement + Stmt::Export(list) => { + for (id, id_pos, rename) in list { + let mut found = false; + + // Mark scope variables as public + match scope.get_index(id) { + Some((index, ScopeEntryType::Normal)) + | Some((index, ScopeEntryType::Constant)) => { + let alias = rename + .as_ref() + .map(|(n, _)| n.clone()) + .unwrap_or_else(|| id.clone()); + scope.set_entry_alias(index, alias); + found = true; + } + Some((_, ScopeEntryType::Module)) => unreachable!(), + _ => (), + } + + if !found { + return Err(Box::new(EvalAltResult::ErrorVariableNotFound( + id.into(), + *id_pos, + ))); + } + } + Ok(Default::default()) + } } } diff --git a/src/error.rs b/src/error.rs index eed98271..ee3f87f7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -98,6 +98,14 @@ pub enum ParseErrorType { /// /// Never appears under the `no_function` feature. FnMissingBody(String), + /// An export statement has duplicated names. + /// + /// Never appears under the `no_module` feature. + DuplicatedExport(String), + /// Export statement not at global level. + /// + /// Never appears under the `no_module` feature. + WrongExport, /// Assignment to a copy of a value. AssignmentToCopy, /// Assignment to an a constant variable. @@ -147,6 +155,8 @@ impl ParseError { 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::LoopBreak => "Break statement should only be used inside a loop" @@ -193,6 +203,12 @@ impl fmt::Display for ParseError { 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() => { diff --git a/src/module.rs b/src/module.rs index a1721835..4eac6dfa 100644 --- a/src/module.rs +++ b/src/module.rs @@ -31,6 +31,7 @@ pub trait ModuleResolver { fn resolve( &self, engine: &Engine, + scope: Scope, path: &str, pos: Position, ) -> Result>; @@ -570,17 +571,15 @@ impl Module { /// use rhai::{Engine, Module}; /// /// let engine = Engine::new(); + /// let mut scope = Scope::new(); /// let ast = engine.compile("let answer = 42;")?; - /// let module = Module::eval_ast_as_new(&ast, &engine)?; + /// let module = Module::eval_ast_as_new(scope, &ast, &engine)?; /// assert!(module.contains_var("answer")); /// assert_eq!(module.get_var_value::("answer").unwrap(), 42); /// # Ok(()) /// # } /// ``` - pub fn eval_ast_as_new(ast: &AST, engine: &Engine) -> FuncReturn { - // Use new scope - let mut scope = Scope::new(); - + pub fn eval_ast_as_new(mut scope: Scope, ast: &AST, engine: &Engine) -> FuncReturn { // Run the script engine.eval_ast_with_scope_raw(&mut scope, &ast)?; @@ -589,13 +588,19 @@ impl Module { scope.into_iter().for_each( |ScopeEntry { - name, typ, value, .. + name, + typ, + value, + alias, + .. }| { match typ { - // Variables left in the scope become module variables - ScopeEntryType::Normal | ScopeEntryType::Constant => { - module.variables.insert(name.into_owned(), value); + // Variables with an alias left in the scope become module variables + ScopeEntryType::Normal | ScopeEntryType::Constant if alias.is_some() => { + module.variables.insert(*alias.unwrap(), value); } + // Variables with no alias are private and not exported + ScopeEntryType::Normal | ScopeEntryType::Constant => (), // Modules left in the scope become sub-modules ScopeEntryType::Module => { module @@ -788,9 +793,10 @@ mod file { pub fn create_module>( &self, engine: &Engine, + scope: Scope, path: &str, ) -> Result> { - self.resolve(engine, path, Default::default()) + self.resolve(engine, scope, path, Default::default()) } } @@ -798,6 +804,7 @@ mod file { fn resolve( &self, engine: &Engine, + scope: Scope, path: &str, pos: Position, ) -> Result> { @@ -811,7 +818,7 @@ mod file { .compile_file(file_path) .map_err(|err| EvalAltResult::set_position(err, pos))?; - Module::eval_ast_as_new(&ast, engine) + Module::eval_ast_as_new(scope, &ast, engine) .map_err(|err| EvalAltResult::set_position(err, pos)) } } @@ -938,6 +945,7 @@ mod stat { fn resolve( &self, _: &Engine, + _: Scope, path: &str, pos: Position, ) -> Result> { diff --git a/src/parser.rs b/src/parser.rs index 2499cdca..ad93943c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -284,6 +284,8 @@ pub enum Stmt { ReturnWithVal(Option>, ReturnType, Position), /// import expr as module Import(Box, Box, Position), + /// expr id as name, ... + Export(Vec<(String, Position, Option<(String, Position)>)>), } impl Stmt { @@ -302,6 +304,8 @@ impl Stmt { Stmt::IfThenElse(expr, _, _) | Stmt::Expr(expr) => expr.position(), Stmt::While(_, stmt) | Stmt::Loop(stmt) | Stmt::For(_, _, stmt) => stmt.position(), + + Stmt::Export(list) => list.get(0).unwrap().1, } } @@ -320,6 +324,7 @@ impl Stmt { Stmt::Let(_, _, _) | Stmt::Const(_, _, _) | Stmt::Import(_, _, _) + | Stmt::Export(_) | Stmt::Expr(_) | Stmt::Continue(_) | Stmt::Break(_) @@ -344,6 +349,7 @@ impl Stmt { Stmt::Block(statements, _) => statements.iter().all(Stmt::is_pure), Stmt::Continue(_) | Stmt::Break(_) | Stmt::ReturnWithVal(_, _, _) => false, Stmt::Import(_, _, _) => false, + Stmt::Export(_) => false, } } } @@ -1970,6 +1976,63 @@ fn parse_import<'a>( Ok(Stmt::Import(Box::new(expr), Box::new(name), pos)) } +/// Parse an export statement. +fn parse_export<'a>(input: &mut Peekable>) -> Result> { + eat_token(input, Token::Export); + + let mut exports = Vec::new(); + + 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)) + } + (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), + }; + + let rename = if match_token(input, Token::As)? { + match input.next().unwrap() { + (Token::Identifier(s), pos) => Some((s.clone(), pos)), + (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), + } + } else { + None + }; + + exports.push((id, id_pos, rename)); + + match input.peek().unwrap() { + (Token::Comma, _) => { + eat_token(input, Token::Comma); + } + (Token::Identifier(_), pos) => { + return Err(PERR::MissingToken( + Token::Comma.into(), + "to separate the list of exports".into(), + ) + .into_err(*pos)) + } + _ => break, + } + } + + // Check for duplicating parameters + exports + .iter() + .enumerate() + .try_for_each(|(i, (p1, _, _))| { + exports + .iter() + .skip(i + 1) + .find(|(p2, _, _)| p2 == p1) + .map_or_else(|| Ok(()), |(p2, pos, _)| Err((p2, *pos))) + }) + .map_err(|(p, pos)| PERR::DuplicatedExport(p.to_string()).into_err(pos))?; + + Ok(Stmt::Export(exports)) +} + /// Parse a statement block. fn parse_block<'a>( input: &mut Peekable>, @@ -1995,7 +2058,7 @@ fn parse_block<'a>( while !match_token(input, Token::RightBrace)? { // Parse statements inside the block - let stmt = parse_stmt(input, stack, breakable, allow_stmt_expr)?; + let stmt = parse_stmt(input, stack, breakable, false, allow_stmt_expr)?; // See if it needs a terminating semicolon let need_semicolon = !stmt.is_self_terminated(); @@ -2053,6 +2116,7 @@ fn parse_stmt<'a>( input: &mut Peekable>, stack: &mut Stack, breakable: bool, + is_global: bool, allow_stmt_expr: bool, ) -> Result> { let (token, pos) = match input.peek().unwrap() { @@ -2113,6 +2177,12 @@ fn parse_stmt<'a>( #[cfg(not(feature = "no_module"))] Token::Import => parse_import(input, stack, allow_stmt_expr), + #[cfg(not(feature = "no_module"))] + Token::Export if !is_global => Err(PERR::WrongExport.into_err(*pos)), + + #[cfg(not(feature = "no_module"))] + Token::Export => parse_export(input), + _ => parse_expr_stmt(input, stack, allow_stmt_expr), } } @@ -2123,7 +2193,7 @@ fn parse_fn<'a>( stack: &mut Stack, allow_stmt_expr: bool, ) -> Result> { - let pos = input.next().expect("should be fn").1; + let pos = eat_token(input, Token::Fn); let name = match input.next().unwrap() { (Token::Identifier(s), _) => s, @@ -2258,7 +2328,7 @@ fn parse_global_level<'a>( } } // Actual statement - let stmt = parse_stmt(input, &mut stack, false, true)?; + let stmt = parse_stmt(input, &mut stack, false, true, true)?; let need_semicolon = !stmt.is_self_terminated(); diff --git a/src/scope.rs b/src/scope.rs index 04e447bd..c15c0581 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -30,6 +30,8 @@ pub struct Entry<'a> { pub typ: EntryType, /// Current value of the entry. pub value: Dynamic, + /// Alias of the entry. + pub alias: Option>, /// A constant expression if the initial value matches one of the recognized types. pub expr: Option>, } @@ -248,6 +250,7 @@ impl<'a> Scope<'a> { self.0.push(Entry { name: name.into(), typ: entry_type, + alias: None, value: value.into(), expr, }); @@ -412,16 +415,15 @@ impl<'a> Scope<'a> { /// Get a mutable reference to an entry in the Scope. pub(crate) fn get_mut(&mut self, index: usize) -> (&mut Dynamic, EntryType) { let entry = self.0.get_mut(index).expect("invalid index in Scope"); - - // assert_ne!( - // entry.typ, - // EntryType::Constant, - // "get mut of constant entry" - // ); - (&mut entry.value, entry.typ) } + /// Update the access type of an entry in the Scope. + pub(crate) fn set_entry_alias(&mut self, index: usize, alias: String) { + let entry = self.0.get_mut(index).expect("invalid index in Scope"); + entry.alias = Some(Box::new(alias)); + } + /// Get an iterator to entries in the Scope. pub(crate) fn into_iter(self) -> impl Iterator> { self.0.into_iter() @@ -439,6 +441,7 @@ impl<'a, K: Into>> iter::Extend<(K, EntryType, Dynamic)> for Scope< .extend(iter.into_iter().map(|(name, typ, value)| Entry { name: name.into(), typ, + alias: None, value: value.into(), expr: None, }));