diff --git a/RELEASES.md b/RELEASES.md index 96c0c55e..af9b3470 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -23,6 +23,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. * `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 70977290..46e15377 100644 --- a/doc/src/language/modules/export.md +++ b/doc/src/language/modules/export.md @@ -32,14 +32,17 @@ let x = 42; // this will be exported below export x; // the variable 'x' is exported under its own name +export let x = 42; // convenient short-hand to declare a variable and export it + // 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' { let inner = 0; // local variable - it disappears when the statement block ends, - // therefore it is not 'global' and is not exported + // therefore it is not 'global' and cannot be exported - export inner; // exporting an temporary variable has no effect + export inner; // <- syntax error: cannot export a local variable } ``` diff --git a/src/ast.rs b/src/ast.rs index 25bdace5..5b89148c 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -608,10 +608,10 @@ pub enum Stmt { Loop(Box, Position), /// for id in expr { stmt } For(Expr, Box<(String, Stmt)>, Position), - /// let id = expr - Let(Box, Option, Position), - /// const id = expr - Const(Box, Option, Position), + /// [export] let id = expr + Let(Box, Option, bool, Position), + /// [export] const id = expr + Const(Box, Option, bool, Position), /// expr op= expr Assignment(Box<(Expr, Cow<'static, str>, Expr)>, Position), /// { stmt; ... } @@ -665,10 +665,10 @@ impl Stmt { | Self::While(_, _, pos) | Self::Loop(_, pos) | Self::For(_, _, pos) - | Self::ReturnWithVal((_, pos), _, _) => *pos, - - Self::Let(x, _, _) | Self::Const(x, _, _) => x.pos, - Self::TryCatch(_, pos, _) => *pos, + | Self::ReturnWithVal((_, pos), _, _) + | Self::Let(_, _, _, pos) + | Self::Const(_, _, _, pos) + | Self::TryCatch(_, pos, _) => *pos, Self::Expr(x) => x.position(), @@ -694,10 +694,10 @@ impl Stmt { | Self::While(_, _, pos) | Self::Loop(_, pos) | Self::For(_, _, pos) - | Self::ReturnWithVal((_, pos), _, _) => *pos = new_pos, - - Self::Let(x, _, _) | Self::Const(x, _, _) => x.pos = new_pos, - Self::TryCatch(_, pos, _) => *pos = new_pos, + | Self::ReturnWithVal((_, pos), _, _) + | Self::Let(_, _, _, pos) + | Self::Const(_, _, _, pos) + | Self::TryCatch(_, pos, _) => *pos = new_pos, Self::Expr(x) => { x.set_position(new_pos); @@ -728,8 +728,8 @@ impl Stmt { // A No-op requires a semicolon in order to know it is an empty statement! Self::Noop(_) => false, - Self::Let(_, _, _) - | Self::Const(_, _, _) + Self::Let(_, _, _, _) + | Self::Const(_, _, _, _) | Self::Assignment(_, _) | Self::Expr(_) | Self::Continue(_) @@ -756,7 +756,7 @@ impl Stmt { Self::While(condition, block, _) => condition.is_pure() && block.is_pure(), Self::Loop(block, _) => block.is_pure(), Self::For(iterable, x, _) => iterable.is_pure() && x.1.is_pure(), - Self::Let(_, _, _) | Self::Const(_, _, _) | Self::Assignment(_, _) => false, + Self::Let(_, _, _, _) | Self::Const(_, _, _, _) | Self::Assignment(_, _) => false, Self::Block(block, _) => block.iter().all(|stmt| stmt.is_pure()), Self::Continue(_) | Self::Break(_) | Self::ReturnWithVal(_, _, _) => false, Self::TryCatch(x, _, _) => x.0.is_pure() && x.2.is_pure(), diff --git a/src/engine.rs b/src/engine.rs index a7f712e6..96a922d7 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -3,7 +3,7 @@ use crate::ast::{BinaryExpr, Expr, FnCallInfo, Ident, IdentX, ReturnType, Stmt}; use crate::dynamic::{map_std_type_name, Dynamic, Union, Variant}; use crate::fn_call::run_builtin_op_assignment; -use crate::fn_native::{shared_try_take, Callback, FnPtr, OnVarCallback, Shared}; +use crate::fn_native::{Callback, FnPtr, OnVarCallback, Shared}; use crate::module::{Module, ModuleRef}; use crate::optimize::OptimizationLevel; use crate::packages::{Package, PackagesCollection, StandardPackage}; @@ -18,10 +18,11 @@ use crate::{calc_native_fn_hash, StaticVec}; use crate::INT; #[cfg(not(feature = "no_module"))] -use crate::module::ModuleResolver; +use crate::{fn_native::shared_try_take, module::ModuleResolver}; #[cfg(not(feature = "no_std"))] #[cfg(not(feature = "no_module"))] +#[cfg(not(target_arch = "wasm32"))] use crate::module::resolvers; #[cfg(any(not(feature = "no_object"), not(feature = "no_module")))] @@ -2092,10 +2093,10 @@ impl Engine { } // Let/const statement - Stmt::Let(var_def, expr, _) | Stmt::Const(var_def, expr, _) => { + Stmt::Let(var_def, expr, export, _) | Stmt::Const(var_def, expr, export, _) => { let entry_type = match stmt { - Stmt::Let(_, _, _) => ScopeEntryType::Normal, - Stmt::Const(_, _, _) => ScopeEntryType::Constant, + Stmt::Let(_, _, _, _) => ScopeEntryType::Normal, + Stmt::Const(_, _, _, _) => ScopeEntryType::Constant, _ => unreachable!(), }; @@ -2105,12 +2106,26 @@ impl Engine { } else { ().into() }; - let var_name: Cow<'_, str> = if state.is_global() { - var_def.name.clone().into() + let (var_name, _alias): (Cow<'_, str>, _) = if state.is_global() { + ( + var_def.name.clone().into(), + if *export { + Some(var_def.name.to_string()) + } else { + None + }, + ) + } else if *export { + unreachable!(); } else { - unsafe_cast_var_name_to_lifetime(&var_def.name).into() + (unsafe_cast_var_name_to_lifetime(&var_def.name).into(), None) }; scope.push_dynamic_value(var_name, entry_type, val); + + #[cfg(not(feature = "no_module"))] + if let Some(alias) = _alias { + scope.set_entry_alias(scope.len() - 1, alias); + } Ok(Default::default()) } diff --git a/src/optimize.rs b/src/optimize.rs index 08d23518..d627faf8 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -266,9 +266,11 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { ) } // let id = expr; - Stmt::Let(name, Some(expr), pos) => Stmt::Let(name, Some(optimize_expr(expr, state)), pos), + Stmt::Let(name, Some(expr), export, pos) => { + Stmt::Let(name, Some(optimize_expr(expr, state)), export, pos) + } // let id; - stmt @ Stmt::Let(_, None, _) => stmt, + stmt @ Stmt::Let(_, None, _, _) => stmt, // import expr as var; #[cfg(not(feature = "no_module"))] Stmt::Import(expr, alias, pos) => Stmt::Import(optimize_expr(expr, state), alias, pos), @@ -282,16 +284,12 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { .into_iter() .map(|stmt| match stmt { // Add constant literals into the state - Stmt::Const(var_def, Some(expr), pos) if expr.is_literal() => { + Stmt::Const(var_def, Some(expr), _, pos) if expr.is_literal() => { state.set_dirty(); state.push_constant(&var_def.name, expr); Stmt::Noop(pos) // No need to keep constants } - Stmt::Const(var_def, Some(expr), pos) if expr.is_literal() => { - let expr = optimize_expr(expr, state); - Stmt::Const(var_def, Some(expr), pos) - } - Stmt::Const(var_def, None, pos) => { + Stmt::Const(var_def, None, _, pos) => { state.set_dirty(); state.push_constant(&var_def.name, Expr::Unit(var_def.pos)); Stmt::Noop(pos) // No need to keep constants @@ -317,7 +315,7 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { while let Some(expr) = result.pop() { match expr { - Stmt::Let(_, expr, _) => { + Stmt::Let(_, expr, _, _) => { removed = expr.as_ref().map(Expr::is_pure).unwrap_or(true) } #[cfg(not(feature = "no_module"))] @@ -375,7 +373,7 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { Stmt::Noop(pos) } // Only one let statement - leave it alone - [x] if matches!(x, Stmt::Let(_, _, _)) => Stmt::Block(result, pos), + [x] if matches!(x, Stmt::Let(_, _, _, _)) => Stmt::Block(result, pos), // Only one import statement - leave it alone #[cfg(not(feature = "no_module"))] [x] if matches!(x, Stmt::Import(_, _, _)) => Stmt::Block(result, pos), @@ -780,7 +778,7 @@ fn optimize( .enumerate() .map(|(i, stmt)| { match stmt { - Stmt::Const(var_def, Some(expr), pos) => { + Stmt::Const(var_def, Some(expr), export, pos) => { // Load constants let expr = optimize_expr(expr, &mut state); @@ -791,12 +789,12 @@ fn optimize( // Keep it in the global scope if expr.is_unit() { state.set_dirty(); - Stmt::Const(var_def, None, pos) + Stmt::Const(var_def, None, export, pos) } else { - Stmt::Const(var_def, Some(expr), pos) + Stmt::Const(var_def, Some(expr), export, pos) } } - Stmt::Const(ref var_def, None, _) => { + Stmt::Const(ref var_def, None, _, _) => { state.push_constant(&var_def.name, Expr::Unit(var_def.pos)); // Keep it in the global scope @@ -806,7 +804,7 @@ fn optimize( // Keep all variable declarations at this level // and always keep the last return value let keep = match stmt { - Stmt::Let(_, _, _) => true, + Stmt::Let(_, _, _, _) => true, #[cfg(not(feature = "no_module"))] Stmt::Import(_, _, _) => true, _ => i == num_statements - 1, diff --git a/src/parser.rs b/src/parser.rs index 55819dd8..e30f66d4 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1918,6 +1918,7 @@ fn parse_let( state: &mut ParseState, lib: &mut FunctionsLib, var_type: ScopeEntryType, + export: bool, mut settings: ParseSettings, ) -> Result { // let/const... (specified in `var_type`) @@ -1938,7 +1939,7 @@ fn parse_let( }; // let name = ... - let init_value = if match_token(input, Token::Equals).0 { + let init_expr = if match_token(input, Token::Equals).0 { // let name = expr Some(parse_expr(input, state, lib, settings.level_up())?) } else { @@ -1949,20 +1950,14 @@ fn parse_let( // let name = expr ScopeEntryType::Normal => { state.stack.push((name.clone(), ScopeEntryType::Normal)); - Ok(Stmt::Let( - Box::new(Ident::new(name, pos)), - init_value, - token_pos, - )) + let ident = Ident::new(name, pos); + Ok(Stmt::Let(Box::new(ident), init_expr, export, token_pos)) } // const name = { expr:constant } ScopeEntryType::Constant => { state.stack.push((name.clone(), ScopeEntryType::Constant)); - Ok(Stmt::Const( - Box::new(Ident::new(name, pos)), - init_value, - token_pos, - )) + let ident = Ident::new(name, pos); + Ok(Stmt::Const(Box::new(ident), init_expr, export, token_pos)) } } } @@ -2013,15 +2008,31 @@ fn parse_import( #[cfg(not(feature = "no_module"))] fn parse_export( input: &mut TokenStream, - _state: &mut ParseState, - _lib: &mut FunctionsLib, + state: &mut ParseState, + lib: &mut FunctionsLib, mut settings: ParseSettings, ) -> Result { let token_pos = eat_token(input, Token::Export); settings.pos = token_pos; #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(_state.max_expr_depth)?; + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + match input.peek().unwrap() { + (Token::Let, pos) => { + let pos = *pos; + let mut stmt = parse_let(input, state, lib, ScopeEntryType::Normal, true, settings)?; + stmt.set_position(pos); + return Ok(stmt); + } + (Token::Const, pos) => { + let pos = *pos; + let mut stmt = parse_let(input, state, lib, ScopeEntryType::Constant, true, settings)?; + stmt.set_position(pos); + return Ok(stmt); + } + _ => (), + } let mut exports = Vec::new(); @@ -2311,8 +2322,10 @@ fn parse_stmt( Token::Try => parse_try_catch(input, state, lib, settings.level_up()).map(Some), - Token::Let => parse_let(input, state, lib, Normal, settings.level_up()).map(Some), - Token::Const => parse_let(input, state, lib, Constant, settings.level_up()).map(Some), + Token::Let => parse_let(input, state, lib, Normal, false, settings.level_up()).map(Some), + Token::Const => { + parse_let(input, state, lib, Constant, false, settings.level_up()).map(Some) + } #[cfg(not(feature = "no_module"))] Token::Import => parse_import(input, state, lib, settings.level_up()).map(Some),