diff --git a/RELEASES.md b/RELEASES.md index af9b3470..759f08b9 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -24,6 +24,7 @@ Enhancements * Modules imported via `import` statements at global level can now be used in functions. There is no longer any need to re-`import` the modules at the beginning of each function block. * `export` keyword can now be tagged onto `let` and `const` statements as a short-hand. +* Variables can now be `export`-ed multiple times under different names. * `index_of`, `==` and `!=` are defined for arrays. * `==` and `!=` are defined for object maps. diff --git a/doc/src/language/modules/export.md b/doc/src/language/modules/export.md index 46e15377..6ff7ea3b 100644 --- a/doc/src/language/modules/export.md +++ b/doc/src/language/modules/export.md @@ -46,6 +46,18 @@ export x as answer; // the variable 'x' is exported under the alias 'answer' } ``` +### Multiple Exports + +One `export` statement can export multiple variables, even under multiple names. + +```rust +// The following exports three variables: +// - 'x' (as 'x' and 'hello') +// - 'y' (as 'foo' and 'bar') +// - 'z' (as 'z') +export x, x as hello, x as world, y as foo, y as bar, z; +``` + Export Functions ---------------- diff --git a/src/engine.rs b/src/engine.rs index 96a922d7..a2c90cb8 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -2124,7 +2124,7 @@ impl Engine { #[cfg(not(feature = "no_module"))] if let Some(alias) = _alias { - scope.set_entry_alias(scope.len() - 1, alias); + scope.add_entry_alias(scope.len() - 1, alias); } Ok(Default::default()) } @@ -2178,7 +2178,7 @@ impl Engine { // Mark scope variables as public if let Some(index) = scope.get_index(name).map(|(i, _)| i) { let alias = rename.as_ref().map(|x| &x.name).unwrap_or_else(|| name); - scope.set_entry_alias(index, alias.clone()); + scope.add_entry_alias(index, alias.clone()); } else { return EvalAltResult::ErrorVariableNotFound(name.into(), *id_pos).into(); } diff --git a/src/module/mod.rs b/src/module/mod.rs index 1474d8fc..3b442415 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -1380,10 +1380,14 @@ impl Module { // Create new module let mut module = Module::new(); - scope.into_iter().for_each(|(_, _, value, alias)| { + scope.into_iter().for_each(|(_, _, value, mut aliases)| { // Variables with an alias left in the scope become module variables - if let Some(alias) = alias { - module.variables.insert(alias, value); + if aliases.len() > 1 { + aliases.into_iter().for_each(|alias| { + module.variables.insert(alias, value.clone()); + }); + } else if aliases.len() == 1 { + module.variables.insert(aliases.pop().unwrap(), value); } }); diff --git a/src/parse_error.rs b/src/parse_error.rs index 23613a24..ee858927 100644 --- a/src/parse_error.rs +++ b/src/parse_error.rs @@ -138,10 +138,6 @@ 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. @@ -190,7 +186,6 @@ impl ParseErrorType { 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::AssignmentToConstant(_) => "Cannot assign to a constant value", Self::AssignmentToInvalidLHS(_) => "Expression cannot be assigned to", @@ -232,12 +227,6 @@ impl fmt::Display for ParseErrorType { 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() => f.write_str(self.desc()), diff --git a/src/parser.rs b/src/parser.rs index e30f66d4..0a69acbb 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2076,19 +2076,6 @@ fn parse_export( } } - // Check for duplicating parameters - exports - .iter() - .enumerate() - .try_for_each(|(i, (Ident { name: id1, .. }, _))| { - exports - .iter() - .skip(i + 1) - .find(|(Ident { name: id2, .. }, _)| id2 == id1) - .map_or_else(|| Ok(()), |(Ident { name: id2, pos }, _)| Err((id2, *pos))) - }) - .map_err(|(id2, pos)| PERR::DuplicatedExport(id2.to_string()).into_err(pos))?; - Ok(Stmt::Export(exports, token_pos)) } diff --git a/src/scope.rs b/src/scope.rs index 2dfff5c9..901755f0 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,6 +1,7 @@ //! Module that defines the `Scope` type representing a function call-stack scope. use crate::dynamic::{Dynamic, Variant}; +use crate::StaticVec; use crate::stdlib::{borrow::Cow, boxed::Box, iter, string::String, vec::Vec}; @@ -70,8 +71,8 @@ pub struct Scope<'a> { values: Vec, /// Type of the entry. types: Vec, - /// (Name, alias) of the entry. The alias is Boxed because it occurs rarely. - names: Vec<(Cow<'a, str>, Option>)>, + /// (Name, aliases) of the entry. The list of aliases is Boxed because it occurs rarely. + names: Vec<(Cow<'a, str>, Box>)>, } impl<'a> Scope<'a> { @@ -248,7 +249,7 @@ impl<'a> Scope<'a> { entry_type: EntryType, value: Dynamic, ) -> &mut Self { - self.names.push((name.into(), None)); + self.names.push((name.into(), Box::new(Default::default()))); self.types.push(entry_type); self.values.push(value.into()); self @@ -387,9 +388,11 @@ impl<'a> Scope<'a> { /// Update the access type of an entry in the Scope. #[cfg(not(feature = "no_module"))] #[inline(always)] - pub(crate) fn set_entry_alias(&mut self, index: usize, alias: String) -> &mut Self { + pub(crate) fn add_entry_alias(&mut self, index: usize, alias: String) -> &mut Self { let entry = self.names.get_mut(index).expect("invalid index in Scope"); - entry.1 = Some(Box::new(alias)); + if !entry.1.contains(&alias) { + entry.1.push(alias); + } self } /// Clone the Scope, keeping only the last instances of each variable name. @@ -416,11 +419,11 @@ impl<'a> Scope<'a> { #[inline(always)] pub(crate) fn into_iter( self, - ) -> impl Iterator, EntryType, Dynamic, Option)> { + ) -> impl Iterator, EntryType, Dynamic, Vec)> { self.names .into_iter() .zip(self.types.into_iter().zip(self.values.into_iter())) - .map(|((name, alias), (typ, value))| (name, typ, value, alias.map(|v| *v))) + .map(|((name, alias), (typ, value))| (name, typ, value, alias.to_vec())) } /// Get an iterator to entries in the Scope. /// Shared values are flatten-cloned. @@ -467,7 +470,7 @@ impl<'a, K: Into>> iter::Extend<(K, EntryType, Dynamic)> for Scope< #[inline(always)] fn extend>(&mut self, iter: T) { iter.into_iter().for_each(|(name, typ, value)| { - self.names.push((name.into(), None)); + self.names.push((name.into(), Box::new(Default::default()))); self.types.push(typ); self.values.push(value); }); diff --git a/tests/modules.rs b/tests/modules.rs index 97496881..5bdbf99c 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -248,7 +248,7 @@ fn test_module_from_ast() -> Result<(), Box> { import "another module" as extra; // Variables defined at global level become module variables - const x = 123; + export const x = 123; let foo = 41; let hello; @@ -258,6 +258,7 @@ fn test_module_from_ast() -> Result<(), Box> { export x as abc, + x as xxx, foo, hello; "#, @@ -275,6 +276,14 @@ fn test_module_from_ast() -> Result<(), Box> { engine.eval::(r#"import "testing" as ttt; ttt::abc"#)?, 123 ); + assert_eq!( + engine.eval::(r#"import "testing" as ttt; ttt::x"#)?, + 123 + ); + assert_eq!( + engine.eval::(r#"import "testing" as ttt; ttt::xxx"#)?, + 123 + ); assert_eq!( engine.eval::(r#"import "testing" as ttt; ttt::foo"#)?, 42