From f6033ac5af75073dddce2400304448dbbadd0318 Mon Sep 17 00:00:00 2001 From: WindSoilder Date: Tue, 1 Aug 2023 07:09:52 +0800 Subject: [PATCH] Module: support defining const and use const variables inside of function (#9773) # Description Relative: #8248 After this pr, user can define const variable inside a module. ![image](https://github.com/nushell/nushell/assets/22256154/e3e03e56-c4b5-4144-a944-d1b20bec1cbd) And user can export const variables, the following screenshot shows how it works (it follows https://github.com/nushell/nushell/issues/8248#issuecomment-1637442612): ![image](https://github.com/nushell/nushell/assets/22256154/b2c14760-3f27-41cc-af77-af70a4367f2a) ## About the change 1. To make module support const, we need to change `parse_module_block` to support `const` keyword. 2. To suport export `const`, we need to make module tracking variables, so we add `variables` attribute to `Module` 3. During eval, the const variable may not exists in `stack`, because we don't eval `const` when we define a module, so we need to find variables which are already registered in `engine_state` ## One more thing to note about the const value. Consider the following code ``` module foo { const b = 3; export def bar [] { $b } } use foo bar const b = 4; bar ``` The result will be 3 (which is defined in module) rather than 4. I think it's expected behavior. It's something like [dynamic binding](https://www.gnu.org/software/emacs/manual/html_node/elisp/Dynamic-Binding-Tips.html) vs [lexical binding](https://www.gnu.org/software/emacs/manual/html_node/elisp/Lexical-Binding.html) in lisp like language, and lexical binding should be right behavior which generates more predicable result, and it doesn't introduce really subtle bugs in nushell code. What if user want dynamic-binding?(For example: the example code returns `4`) There is no way to do this, user should consider passing the value as argument to custom command rather than const. ## TODO - [X] adding tests for the feature. - [X] support export const out of module to use. # User-Facing Changes # Tests + Formatting # After Submitting --- .../src/core_commands/export_const.rs | 66 ++++++++++++++ crates/nu-cmd-lang/src/core_commands/mod.rs | 2 + crates/nu-cmd-lang/src/core_commands/use_.rs | 82 ++++++++++++++++- crates/nu-cmd-lang/src/default_context.rs | 1 + crates/nu-engine/src/eval.rs | 8 ++ crates/nu-parser/src/parse_keywords.rs | 90 +++++++++++++++++-- crates/nu-parser/src/parser.rs | 1 + crates/nu-protocol/src/ast/import_pattern.rs | 5 +- crates/nu-protocol/src/engine/engine_state.rs | 19 +++- crates/nu-protocol/src/engine/overlay.rs | 4 + crates/nu-protocol/src/exportable.rs | 3 +- crates/nu-protocol/src/module.rs | 76 +++++++++++++--- src/tests/test_modules.rs | 44 +++++++++ 13 files changed, 377 insertions(+), 24 deletions(-) create mode 100644 crates/nu-cmd-lang/src/core_commands/export_const.rs diff --git a/crates/nu-cmd-lang/src/core_commands/export_const.rs b/crates/nu-cmd-lang/src/core_commands/export_const.rs new file mode 100644 index 0000000000..5d30b42728 --- /dev/null +++ b/crates/nu-cmd-lang/src/core_commands/export_const.rs @@ -0,0 +1,66 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, +}; + +#[derive(Clone)] +pub struct ExportConst; + +impl Command for ExportConst { + fn name(&self) -> &str { + "export const" + } + + fn usage(&self) -> &str { + "Use parse-time constant from a module and export them from this module." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("export const") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .allow_variants_without_examples(true) + .required("const_name", SyntaxShape::VarWithOptType, "constant name") + .required( + "initial_value", + SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::MathExpression)), + "equals sign followed by constant value", + ) + .category(Category::Core) + } + + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn is_parser_keyword(&self) -> bool { + true + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Re-export a command from another module", + example: r#"module spam { export const foo = 3; } + module eggs { export use spam foo } + use eggs foo + foo + "#, + result: Some(Value::test_int(3)), + }] + } + + fn search_terms(&self) -> Vec<&str> { + vec!["reexport", "import", "module"] + } +} diff --git a/crates/nu-cmd-lang/src/core_commands/mod.rs b/crates/nu-cmd-lang/src/core_commands/mod.rs index 2933126f12..8b0f2d33ec 100644 --- a/crates/nu-cmd-lang/src/core_commands/mod.rs +++ b/crates/nu-cmd-lang/src/core_commands/mod.rs @@ -11,6 +11,7 @@ mod echo; mod error_make; mod export; mod export_alias; +mod export_const; mod export_def; mod export_def_env; mod export_extern; @@ -50,6 +51,7 @@ pub use echo::Echo; pub use error_make::ErrorMake; pub use export::ExportCommand; pub use export_alias::ExportAlias; +pub use export_const::ExportConst; pub use export_def::ExportDef; pub use export_def_env::ExportDefEnv; pub use export_extern::ExportExtern; diff --git a/crates/nu-cmd-lang/src/core_commands/use_.rs b/crates/nu-cmd-lang/src/core_commands/use_.rs index f500d7b377..508cb78cbc 100644 --- a/crates/nu-cmd-lang/src/core_commands/use_.rs +++ b/crates/nu-cmd-lang/src/core_commands/use_.rs @@ -1,8 +1,8 @@ use nu_engine::{eval_block, find_in_dirs_env, get_dirs_var_from_call, redirect_env}; -use nu_protocol::ast::{Call, Expr, Expression}; +use nu_protocol::ast::{Call, Expr, Expression, ImportPattern, ImportPatternMember}; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ - Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, + Category, Example, Module, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, }; #[derive(Clone)] @@ -110,6 +110,14 @@ This command is a parser keyword. For details, check: // Merge the block's environment to the current stack redirect_env(engine_state, caller_stack, &callee_stack); } + + use_variables( + engine_state, + import_pattern, + module, + caller_stack, + call.head, + ); } else { return Err(ShellError::GenericError( format!( @@ -162,6 +170,76 @@ This command is a parser keyword. For details, check: } } +fn use_variables( + engine_state: &EngineState, + import_pattern: &ImportPattern, + module: &Module, + caller_stack: &mut Stack, + head_span: Span, +) { + if !module.variables.is_empty() { + if import_pattern.members.is_empty() { + // add a record variable. + if let Some(var_id) = import_pattern.module_name_var_id { + let mut cols = vec![]; + let mut vals = vec![]; + for (var_name, var_id) in module.variables.iter() { + if let Some(val) = engine_state.get_var(*var_id).clone().const_val { + cols.push(String::from_utf8_lossy(var_name).to_string()); + vals.push(val) + } + } + caller_stack.add_var( + var_id, + Value::record(cols, vals, module.span.unwrap_or(head_span)), + ) + } + } else { + let mut have_glob = false; + for m in &import_pattern.members { + if matches!(m, ImportPatternMember::Glob { .. }) { + have_glob = true; + break; + } + } + if have_glob { + // bring all variables into scope directly. + for (_, var_id) in module.variables.iter() { + if let Some(val) = engine_state.get_var(*var_id).clone().const_val { + caller_stack.add_var(*var_id, val); + } + } + } else { + let mut members = vec![]; + for m in &import_pattern.members { + match m { + ImportPatternMember::List { names, .. } => { + for (n, _) in names { + if module.variables.contains_key(n) { + members.push(n); + } + } + } + ImportPatternMember::Name { name, .. } => { + if module.variables.contains_key(name) { + members.push(name) + } + } + ImportPatternMember::Glob { .. } => continue, + } + } + for m in members { + if let Some(var_id) = module.variables.get(m) { + if let Some(val) = engine_state.get_var(*var_id).clone().const_val { + caller_stack.add_var(*var_id, val); + } + } + } + } + } + } +} + #[cfg(test)] mod test { #[test] diff --git a/crates/nu-cmd-lang/src/default_context.rs b/crates/nu-cmd-lang/src/default_context.rs index 1f3ffbc749..25f90360ce 100644 --- a/crates/nu-cmd-lang/src/default_context.rs +++ b/crates/nu-cmd-lang/src/default_context.rs @@ -29,6 +29,7 @@ pub fn create_default_context() -> EngineState { ErrorMake, ExportAlias, ExportCommand, + ExportConst, ExportDef, ExportDefEnv, ExportExtern, diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index 8e0b1bae6c..33bf06ed6a 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -58,6 +58,14 @@ pub fn eval_call( let block = engine_state.get_block(block_id); let mut callee_stack = caller_stack.gather_captures(&block.captures); + // When the def is defined in module, relative captured variable doesn't go into stack + // so it can't be merged to callee_stack, but the variable is defined in `engine_state` + // then, to solve the issue, we also need to try to get relative const from `engine_state` + for cap in &block.captures { + if let Some(value) = engine_state.get_var(*cap).const_val.clone() { + callee_stack.vars.push((*cap, value)) + } + } for (param_idx, param) in decl .signature() diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index 6a497720cc..1f8dd5988e 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -940,9 +940,8 @@ pub fn parse_export_in_block( let full_name = if lite_command.parts.len() > 1 { let sub = working_set.get_span_contents(lite_command.parts[1]); match sub { - b"alias" | b"def" | b"def-env" | b"extern" | b"extern-wrapped" | b"use" | b"module" => { - [b"export ", sub].concat() - } + b"alias" | b"def" | b"def-env" | b"extern" | b"extern-wrapped" | b"use" | b"module" + | b"const" => [b"export ", sub].concat(), _ => b"export".to_vec(), } } else { @@ -1000,6 +999,7 @@ pub fn parse_export_in_block( match full_name.as_slice() { b"export alias" => parse_alias(working_set, lite_command, None), b"export def" | b"export def-env" => parse_def(working_set, lite_command, None), + b"export const" => parse_const(working_set, &lite_command.parts[1..]), b"export use" => { let (pipeline, _) = parse_use(working_set, &lite_command.parts); pipeline @@ -1400,6 +1400,59 @@ pub fn parse_export_in_module( result } + b"const" => { + let pipeline = parse_const(working_set, &spans[1..]); + let export_def_decl_id = if let Some(id) = working_set.find_decl(b"export const") { + id + } else { + working_set.error(ParseError::InternalError( + "missing 'export const' command".into(), + export_span, + )); + return (garbage_pipeline(spans), vec![]); + }; + + // Trying to warp the 'const' call into the 'export const' in a very clumsy way + if let Some(PipelineElement::Expression( + _, + Expression { + expr: Expr::Call(ref def_call), + .. + }, + )) = pipeline.elements.get(0) + { + call = def_call.clone(); + + call.head = span(&spans[0..=1]); + call.decl_id = export_def_decl_id; + } else { + working_set.error(ParseError::InternalError( + "unexpected output from parsing a definition".into(), + span(&spans[1..]), + )); + }; + + let mut result = vec![]; + + if let Some(decl_name_span) = spans.get(2) { + let decl_name = working_set.get_span_contents(*decl_name_span); + let decl_name = trim_quotes(decl_name); + + if let Some(decl_id) = working_set.find_variable(decl_name) { + result.push(Exportable::VarDecl { + name: decl_name.to_vec(), + id: decl_id, + }); + } else { + working_set.error(ParseError::InternalError( + "failed to find added variable".into(), + span(&spans[1..]), + )); + } + } + + result + } _ => { working_set.error(ParseError::Expected( "def, def-env, alias, use, module, or extern keyword", @@ -1589,6 +1642,9 @@ pub fn parse_module_block( None, // using commands named as the module locally is OK )) } + b"const" => block + .pipelines + .push(parse_const(working_set, &command.parts)), b"extern" | b"extern-wrapped" => { block .pipelines @@ -1713,6 +1769,9 @@ pub fn parse_module_block( module.add_submodule(name, id); } } + Exportable::VarDecl { name, id } => { + module.add_variable(name, id); + } } } @@ -1730,7 +1789,7 @@ pub fn parse_module_block( } _ => { working_set.error(ParseError::ExpectedKeyword( - "def, def-env, extern, alias, use, module, export or export-env keyword".into(), + "def, const, def-env, extern, alias, use, module, export or export-env keyword".into(), command.parts[0], )); @@ -2208,7 +2267,7 @@ pub fn parse_use(working_set: &mut StateWorkingSet, spans: &[Span]) -> (Pipeline return (garbage_pipeline(spans), vec![]); }; - let (import_pattern, module, module_id) = if let Some(module_id) = import_pattern.head.id { + let (mut import_pattern, module, module_id) = if let Some(module_id) = import_pattern.head.id { let module = working_set.get_module(module_id).clone(); ( ImportPattern { @@ -2219,6 +2278,7 @@ pub fn parse_use(working_set: &mut StateWorkingSet, spans: &[Span]) -> (Pipeline }, members: import_pattern.members, hidden: HashSet::new(), + module_name_var_id: None, }, module, module_id, @@ -2239,6 +2299,7 @@ pub fn parse_use(working_set: &mut StateWorkingSet, spans: &[Span]) -> (Pipeline }, members: import_pattern.members, hidden: HashSet::new(), + module_name_var_id: None, }, module, module_id, @@ -2276,12 +2337,29 @@ pub fn parse_use(working_set: &mut StateWorkingSet, spans: &[Span]) -> (Pipeline id: *module_id, }), ) + .chain( + definitions + .variables + .iter() + .map(|(name, variable_id)| Exportable::VarDecl { + name: name.clone(), + id: *variable_id, + }), + ) .collect(); // Extend the current scope with the module's exportables working_set.use_decls(definitions.decls); working_set.use_modules(definitions.modules); + working_set.use_variables(definitions.variables); + let module_name_var_id = working_set.add_variable( + module.name(), + module.span.unwrap_or(Span::unknown()), + Type::Any, + false, + ); + import_pattern.module_name_var_id = Some(module_name_var_id); // Create a new Use command call to pass the import pattern as parser info let import_pattern_expr = Expression { expr: Expr::ImportPattern(import_pattern), @@ -2703,7 +2781,7 @@ pub fn parse_overlay_use(working_set: &mut StateWorkingSet, call: Box) -> ) } } else { - (ResolvedImportPattern::new(vec![], vec![]), vec![]) + (ResolvedImportPattern::new(vec![], vec![], vec![]), vec![]) }; if errors.is_empty() { diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 4f8159b970..89f33acf17 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -2976,6 +2976,7 @@ pub fn parse_import_pattern(working_set: &mut StateWorkingSet, spans: &[Span]) - }, members: vec![], hidden: HashSet::new(), + module_name_var_id: None, }; if spans.len() > 1 { diff --git a/crates/nu-protocol/src/ast/import_pattern.rs b/crates/nu-protocol/src/ast/import_pattern.rs index 1f1fa5712b..3adb144935 100644 --- a/crates/nu-protocol/src/ast/import_pattern.rs +++ b/crates/nu-protocol/src/ast/import_pattern.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{span, ModuleId, Span}; +use crate::{span, ModuleId, Span, VarId}; use std::collections::HashSet; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -24,6 +24,7 @@ pub struct ImportPattern { // communicate to eval which decls/aliases were hidden during `parse_hide()` so it does not // interpret these as env var names: pub hidden: HashSet>, + pub module_name_var_id: Option, } impl ImportPattern { @@ -36,6 +37,7 @@ impl ImportPattern { }, members: vec![], hidden: HashSet::new(), + module_name_var_id: None, } } @@ -62,6 +64,7 @@ impl ImportPattern { head: self.head, members: self.members, hidden, + module_name_var_id: self.module_name_var_id, } } } diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index ce46ed1c09..d6c56eb68b 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -1161,6 +1161,17 @@ impl<'a> StateWorkingSet<'a> { } } + pub fn use_variables(&mut self, variables: Vec<(Vec, VarId)>) { + let overlay_frame = self.last_overlay_mut(); + + for (mut name, var_id) in variables { + if !name.starts_with(b"$") { + name.insert(0, b'$'); + } + overlay_frame.insert_variable(name, var_id); + } + } + pub fn add_predecl(&mut self, decl: Box) -> Option { let name = decl.name().as_bytes().to_vec(); @@ -1530,11 +1541,15 @@ impl<'a> StateWorkingSet<'a> { } pub fn find_variable(&self, name: &[u8]) -> Option { + let mut name = name.to_vec(); + if !name.starts_with(b"$") { + name.insert(0, b'$'); + } let mut removed_overlays = vec![]; for scope_frame in self.delta.scope.iter().rev() { for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() { - if let Some(var_id) = overlay_frame.vars.get(name) { + if let Some(var_id) = overlay_frame.vars.get(&name) { return Some(*var_id); } } @@ -1545,7 +1560,7 @@ impl<'a> StateWorkingSet<'a> { .active_overlays(&removed_overlays) .rev() { - if let Some(var_id) = overlay_frame.vars.get(name) { + if let Some(var_id) = overlay_frame.vars.get(&name) { return Some(*var_id); } } diff --git a/crates/nu-protocol/src/engine/overlay.rs b/crates/nu-protocol/src/engine/overlay.rs index f4884d3b6f..d28a90479f 100644 --- a/crates/nu-protocol/src/engine/overlay.rs +++ b/crates/nu-protocol/src/engine/overlay.rs @@ -206,6 +206,10 @@ impl OverlayFrame { self.modules.insert(name, module_id) } + pub fn insert_variable(&mut self, name: Vec, variable_id: VarId) -> Option { + self.vars.insert(name, variable_id) + } + pub fn get_decl(&self, name: &[u8]) -> Option { self.decls.get(name).cloned() } diff --git a/crates/nu-protocol/src/exportable.rs b/crates/nu-protocol/src/exportable.rs index 8be71a645c..4d2c4922b5 100644 --- a/crates/nu-protocol/src/exportable.rs +++ b/crates/nu-protocol/src/exportable.rs @@ -1,6 +1,7 @@ -use crate::{DeclId, ModuleId}; +use crate::{DeclId, ModuleId, VarId}; pub enum Exportable { Decl { name: Vec, id: DeclId }, Module { name: Vec, id: ModuleId }, + VarDecl { name: Vec, id: VarId }, } diff --git a/crates/nu-protocol/src/module.rs b/crates/nu-protocol/src/module.rs index c46174ace0..dad724e492 100644 --- a/crates/nu-protocol/src/module.rs +++ b/crates/nu-protocol/src/module.rs @@ -1,5 +1,6 @@ use crate::{ ast::ImportPatternMember, engine::StateWorkingSet, BlockId, DeclId, ModuleId, ParseError, Span, + VarId, }; use indexmap::IndexMap; @@ -7,11 +8,20 @@ use indexmap::IndexMap; pub struct ResolvedImportPattern { pub decls: Vec<(Vec, DeclId)>, pub modules: Vec<(Vec, ModuleId)>, + pub variables: Vec<(Vec, VarId)>, } impl ResolvedImportPattern { - pub fn new(decls: Vec<(Vec, DeclId)>, modules: Vec<(Vec, ModuleId)>) -> Self { - ResolvedImportPattern { decls, modules } + pub fn new( + decls: Vec<(Vec, DeclId)>, + modules: Vec<(Vec, ModuleId)>, + variables: Vec<(Vec, VarId)>, + ) -> Self { + ResolvedImportPattern { + decls, + modules, + variables, + } } } @@ -21,6 +31,7 @@ pub struct Module { pub name: Vec, pub decls: IndexMap, DeclId>, pub submodules: IndexMap, ModuleId>, + pub variables: IndexMap, VarId>, pub env_block: Option, // `export-env { ... }` block pub main: Option, // `export def main` pub span: Option, @@ -32,6 +43,7 @@ impl Module { name, decls: IndexMap::new(), submodules: IndexMap::new(), + variables: IndexMap::new(), env_block: None, main: None, span: None, @@ -43,6 +55,7 @@ impl Module { name, decls: IndexMap::new(), submodules: IndexMap::new(), + variables: IndexMap::new(), env_block: None, main: None, span: Some(span), @@ -61,6 +74,10 @@ impl Module { self.submodules.insert(name, module_id) } + pub fn add_variable(&mut self, name: Vec, var_id: VarId) -> Option { + self.variables.insert(name, var_id) + } + pub fn add_env_block(&mut self, block_id: BlockId) { self.env_block = Some(block_id); } @@ -87,7 +104,8 @@ impl Module { (head, rest) } else { // Import pattern was just name without any members - let mut results = vec![]; + let mut decls = vec![]; + let mut vars = vec![]; let mut errors = vec![]; for (_, id) in &self.submodules { @@ -101,14 +119,19 @@ impl Module { new_name.push(b' '); new_name.extend(sub_name); - results.push((new_name, sub_decl_id)); + decls.push((new_name, sub_decl_id)); + } + + for (sub_name, sub_var_id) in sub_results.variables { + vars.push((sub_name, sub_var_id)); } } - results.extend(self.decls_with_head(&final_name)); + decls.extend(self.decls_with_head(&final_name)); + vars.extend(self.vars()); return ( - ResolvedImportPattern::new(results, vec![(final_name, self_id)]), + ResolvedImportPattern::new(decls, vec![(final_name, self_id)], vars), errors, ); }; @@ -118,18 +141,27 @@ impl Module { if name == b"main" { if let Some(main_decl_id) = self.main { ( - ResolvedImportPattern::new(vec![(final_name, main_decl_id)], vec![]), + ResolvedImportPattern::new( + vec![(final_name, main_decl_id)], + vec![], + vec![], + ), vec![], ) } else { ( - ResolvedImportPattern::new(vec![], vec![]), + ResolvedImportPattern::new(vec![], vec![], vec![]), vec![ParseError::ExportNotFound(*span)], ) } } else if let Some(decl_id) = self.decls.get(name) { ( - ResolvedImportPattern::new(vec![(name.clone(), *decl_id)], vec![]), + ResolvedImportPattern::new(vec![(name.clone(), *decl_id)], vec![], vec![]), + vec![], + ) + } else if let Some(var_id) = self.variables.get(name) { + ( + ResolvedImportPattern::new(vec![], vec![], vec![(name.clone(), *var_id)]), vec![], ) } else if let Some(submodule_id) = self.submodules.get(name) { @@ -137,7 +169,7 @@ impl Module { submodule.resolve_import_pattern(working_set, *submodule_id, rest, None) } else { ( - ResolvedImportPattern::new(vec![], vec![]), + ResolvedImportPattern::new(vec![], vec![], vec![]), vec![ParseError::ExportNotFound(*span)], ) } @@ -145,6 +177,7 @@ impl Module { ImportPatternMember::Glob { .. } => { let mut decls = vec![]; let mut submodules = vec![]; + let mut variables = vec![]; let mut errors = vec![]; for (_, id) in &self.submodules { @@ -152,18 +185,25 @@ impl Module { let (sub_results, sub_errors) = submodule.resolve_import_pattern(working_set, *id, &[], None); decls.extend(sub_results.decls); + submodules.extend(sub_results.modules); + variables.extend(sub_results.variables); errors.extend(sub_errors); } decls.extend(self.decls()); + variables.extend(self.variables.clone()); submodules.extend(self.submodules()); - (ResolvedImportPattern::new(decls, submodules), errors) + ( + ResolvedImportPattern::new(decls, submodules, variables), + errors, + ) } ImportPatternMember::List { names } => { let mut decls = vec![]; let mut submodules = vec![]; + let mut variables = vec![]; let mut errors = vec![]; for (name, span) in names { @@ -175,6 +215,8 @@ impl Module { } } else if let Some(decl_id) = self.decls.get(name) { decls.push((name.clone(), *decl_id)); + } else if let Some(var_id) = self.variables.get(name) { + variables.push((name.clone(), *var_id)); } else if let Some(submodule_id) = self.submodules.get(name) { submodules.push((name.clone(), *submodule_id)); } else { @@ -182,7 +224,10 @@ impl Module { } } - (ResolvedImportPattern::new(decls, submodules), errors) + ( + ResolvedImportPattern::new(decls, submodules, variables), + errors, + ) } } } @@ -217,6 +262,13 @@ impl Module { result } + pub fn vars(&self) -> Vec<(Vec, VarId)> { + self.variables + .iter() + .map(|(name, id)| (name.to_vec(), *id)) + .collect() + } + pub fn decl_names_with_head(&self, head: &[u8]) -> Vec> { let mut result: Vec> = self .decls diff --git a/src/tests/test_modules.rs b/src/tests/test_modules.rs index 5481b93e8f..26ca62bf49 100644 --- a/src/tests/test_modules.rs +++ b/src/tests/test_modules.rs @@ -111,3 +111,47 @@ fn export_alias() -> TestResult { "hello", ) } + +#[test] +fn export_consts() -> TestResult { + run_test( + r#"module spam { export const b = 3; }; use spam b; $b"#, + "3", + ) +} + +#[test] +fn func_use_consts() -> TestResult { + run_test( + r#"module spam { const b = 3; export def c [] { $b } }; use spam; spam c"#, + "3", + ) +} + +#[test] +fn export_module_which_defined_const() -> TestResult { + run_test( + r#"module spam { export const b = 3; export const c = 4 }; use spam; $spam.b + $spam.c"#, + "7", + ) +} + +#[test] +fn cannot_export_private_const() -> TestResult { + fail_test( + r#"module spam { const b = 3; export const c = 4 }; use spam; $spam.b + $spam.c"#, + "cannot find column 'b'", + ) +} + +#[test] +fn test_lexical_binding() -> TestResult { + run_test( + r#"module spam { const b = 3; export def c [] { $b } }; use spam c; const b = 4; c"#, + "3", + )?; + run_test( + r#"const b = 4; module spam { const b = 3; export def c [] { $b } }; use spam; spam c"#, + "3", + ) +}