diff --git a/crates/nu-command/src/core_commands/export_def.rs b/crates/nu-command/src/core_commands/export_def.rs new file mode 100644 index 000000000..b82418c48 --- /dev/null +++ b/crates/nu-command/src/core_commands/export_def.rs @@ -0,0 +1,35 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EvaluationContext}; +use nu_protocol::{Signature, SyntaxShape, Value}; + +pub struct ExportDef; + +impl Command for ExportDef { + fn name(&self) -> &str { + "export def" + } + + fn usage(&self) -> &str { + "Define a custom command and export it from a module" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("export def") + .required("target", SyntaxShape::String, "definition name") + .required("params", SyntaxShape::Signature, "parameters") + .required( + "block", + SyntaxShape::Block(Some(vec![])), + "body of the definition", + ) + } + + fn run( + &self, + _context: &EvaluationContext, + call: &Call, + _input: Value, + ) -> Result { + Ok(Value::Nothing { span: call.head }) + } +} diff --git a/crates/nu-command/src/core_commands/hide.rs b/crates/nu-command/src/core_commands/hide.rs new file mode 100644 index 000000000..9c9d611e1 --- /dev/null +++ b/crates/nu-command/src/core_commands/hide.rs @@ -0,0 +1,28 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EvaluationContext}; +use nu_protocol::{Signature, SyntaxShape, Value}; + +pub struct Hide; + +impl Command for Hide { + fn name(&self) -> &str { + "hide" + } + + fn usage(&self) -> &str { + "Hide definitions in the current scope" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("hide").required("pattern", SyntaxShape::String, "import pattern") + } + + fn run( + &self, + _context: &EvaluationContext, + call: &Call, + _input: Value, + ) -> Result { + Ok(Value::Nothing { span: call.head }) + } +} diff --git a/crates/nu-command/src/core_commands/mod.rs b/crates/nu-command/src/core_commands/mod.rs index 4d909467d..3472f4d8b 100644 --- a/crates/nu-command/src/core_commands/mod.rs +++ b/crates/nu-command/src/core_commands/mod.rs @@ -1,7 +1,9 @@ mod alias; mod def; mod do_; +mod export_def; mod help; +mod hide; mod if_; mod let_; mod module; @@ -10,7 +12,9 @@ mod use_; pub use alias::Alias; pub use def::Def; pub use do_::Do; +pub use export_def::ExportDef; pub use help::Help; +pub use hide::Hide; pub use if_::If; pub use let_::Let; pub use module::Module; diff --git a/crates/nu-command/src/core_commands/use_.rs b/crates/nu-command/src/core_commands/use_.rs index 30b5e3d0b..3cfae2e49 100644 --- a/crates/nu-command/src/core_commands/use_.rs +++ b/crates/nu-command/src/core_commands/use_.rs @@ -14,7 +14,7 @@ impl Command for Use { } fn signature(&self) -> nu_protocol::Signature { - Signature::build("use").required("module_name", SyntaxShape::String, "module name") + Signature::build("use").required("pattern", SyntaxShape::String, "import pattern") } fn run( diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index dc119edc5..6d6b474d0 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -19,12 +19,14 @@ pub fn create_default_context() -> Rc> { working_set.add_decl(Box::new(Def)); working_set.add_decl(Box::new(Do)); working_set.add_decl(Box::new(Each)); + working_set.add_decl(Box::new(ExportDef)); working_set.add_decl(Box::new(External)); working_set.add_decl(Box::new(For)); working_set.add_decl(Box::new(From)); working_set.add_decl(Box::new(FromJson)); working_set.add_decl(Box::new(Get)); working_set.add_decl(Box::new(Help)); + working_set.add_decl(Box::new(Hide)); working_set.add_decl(Box::new(If)); working_set.add_decl(Box::new(Length)); working_set.add_decl(Box::new(Let)); diff --git a/crates/nu-parser/src/errors.rs b/crates/nu-parser/src/errors.rs index 58123aac0..444873d1a 100644 --- a/crates/nu-parser/src/errors.rs +++ b/crates/nu-parser/src/errors.rs @@ -57,6 +57,14 @@ pub enum ParseError { #[diagnostic(code(nu::parser::expected_keyword), url(docsrs))] ExpectedKeyword(String, #[label("expected {0}")] Span), + #[error("Unexpected keyword.")] + #[diagnostic( + code(nu::parser::unexpected_keyword), + url(docsrs), + help("'export' keyword is allowed only in a module.") + )] + UnexpectedKeyword(String, #[label("unexpected {0}")] Span), + #[error("Multiple rest params.")] #[diagnostic(code(nu::parser::multiple_rest_params), url(docsrs))] MultipleRestParams(#[label = "multiple rest params"] Span), @@ -69,6 +77,10 @@ pub enum ParseError { #[diagnostic(code(nu::parser::module_not_found), url(docsrs))] ModuleNotFound(#[label = "module not found"] Span), + #[error("Duplicate command definition within a block.")] + #[diagnostic(code(nu::parser::duplicate_command_def), url(docsrs))] + DuplicateCommandDef(#[label = "defined more than once"] Span), + #[error("Unknown command.")] #[diagnostic( code(nu::parser::unknown_command), diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index e516da309..033a21e73 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -13,9 +13,16 @@ use crate::{ ParseError, }; -pub fn parse_def_predecl(working_set: &mut StateWorkingSet, spans: &[Span]) { +pub fn parse_def_predecl(working_set: &mut StateWorkingSet, spans: &[Span]) -> Option { let name = working_set.get_span_contents(spans[0]); + // handle "export def" same as "def" + let (name, spans) = if name == b"export" && spans.len() >= 2 { + (working_set.get_span_contents(spans[1]), &spans[1..]) + } else { + (name, spans) + }; + if name == b"def" && spans.len() >= 4 { let (name_expr, ..) = parse_string(working_set, spans[1]); let name = name_expr.as_string(); @@ -35,9 +42,13 @@ pub fn parse_def_predecl(working_set: &mut StateWorkingSet, spans: &[Span]) { signature.name = name; let decl = signature.predeclare(); - working_set.add_decl(decl); + if working_set.add_predecl(decl).is_some() { + return Some(ParseError::DuplicateCommandDef(spans[1])); + } } } + + None } pub fn parse_def( @@ -88,17 +99,22 @@ pub fn parse_def( call.positional.push(block); if let (Some(name), Some(mut signature), Some(block_id)) = - (name, signature, block_id) + (&name, signature, block_id) { - let decl_id = working_set - .find_decl(name.as_bytes()) - .expect("internal error: predeclaration failed to add definition"); + if let Some(decl_id) = working_set.find_decl(name.as_bytes()) { + let declaration = working_set.get_decl_mut(decl_id); - let declaration = working_set.get_decl_mut(decl_id); + signature.name = name.clone(); - signature.name = name; - - *declaration = signature.into_block_command(block_id); + *declaration = signature.into_block_command(block_id); + } else { + error = error.or_else(|| { + Some(ParseError::UnknownState( + "Could not define hidden command".into(), + spans[1], + )) + }); + }; } } else { let err_span = Span { @@ -111,6 +127,19 @@ pub fn parse_def( } working_set.exit_scope(); + if let Some(name) = name { + // It's OK if it returns None: The decl was already merged in previous parse + // pass. + working_set.merge_predecl(name.as_bytes()); + } else { + error = error.or_else(|| { + Some(ParseError::UnknownState( + "Could not get string from string expression".into(), + *name_span, + )) + }); + } + call } else { let err_span = Span { @@ -218,6 +247,71 @@ pub fn parse_alias( ) } +pub fn parse_export( + working_set: &mut StateWorkingSet, + spans: &[Span], +) -> (Statement, Option) { + let bytes = working_set.get_span_contents(spans[0]); + + if bytes == b"export" && spans.len() >= 3 { + let export_name = working_set.get_span_contents(spans[1]); + + match export_name { + b"def" => { + let (stmt, err) = parse_def(working_set, &spans[1..]); + + let export_def_decl_id = working_set + .find_decl(b"export def") + .expect("internal error: missing 'export def' command"); + + // Trying to warp the 'def' call into the 'export def' in a very clumsy way + let stmt = if let Statement::Pipeline(ref pipe) = stmt { + if !pipe.expressions.is_empty() { + if let Expr::Call(ref call) = pipe.expressions[0].expr { + let mut call = call.clone(); + + call.head = span(&spans[0..=1]); + call.decl_id = export_def_decl_id; + + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: span(spans), + ty: Type::Unknown, + custom_completion: None, + }])) + } else { + stmt + } + } else { + stmt + } + } else { + stmt + }; + + (stmt, err) + } + _ => ( + garbage_statement(spans), + Some(ParseError::Expected( + // TODO: Fill in more as they come + "def keyword".into(), + spans[1], + )), + ), + } + } else { + ( + garbage_statement(spans), + Some(ParseError::UnknownState( + // TODO: fill in more as they come + "Expected structure: export def [] {}".into(), + span(spans), + )), + ) + } +} + pub fn parse_module( working_set: &mut StateWorkingSet, spans: &[Span], @@ -307,15 +401,21 @@ pub fn parse_module( b"def" => { let (stmt, err) = parse_def(working_set, &pipeline.commands[0].parts); + (stmt, err) + } + b"export" => { + let (stmt, err) = + parse_export(working_set, &pipeline.commands[0].parts); + if err.is_none() { let decl_name = - working_set.get_span_contents(pipeline.commands[0].parts[1]); + // parts[2] is safe since it's checked in parse_def already + working_set.get_span_contents(pipeline.commands[0].parts[2]); let decl_id = working_set .find_decl(decl_name) .expect("internal error: failed to find added declaration"); - // TODO: Later, we want to put this behind 'export' exports.push((decl_name.into(), decl_id)); } @@ -324,7 +424,8 @@ pub fn parse_module( _ => ( garbage_statement(&pipeline.commands[0].parts), Some(ParseError::Expected( - "def".into(), + // TODO: Fill in more as they com + "def or export keyword".into(), pipeline.commands[0].parts[0], )), ), @@ -393,8 +494,6 @@ pub fn parse_use( let mut error = None; let bytes = working_set.get_span_contents(spans[0]); - // TODO: Currently, this directly imports the module's definitions into the current scope. - // Later, we want to put them behind the module's name and add selective importing if bytes == b"use" && spans.len() >= 2 { let (module_name_expr, err) = parse_string(working_set, spans[1]); error = error.or(err); @@ -403,8 +502,6 @@ pub fn parse_use( error = error.or(err); let exports = if let Some(block_id) = working_set.find_module(&import_pattern.head) { - // TODO: Since we don't use the Block at all, we might just as well create a separate - // Module that holds only the exports, without having Blocks in the way. working_set.get_block(block_id).exports.clone() } else { return ( @@ -492,6 +589,57 @@ pub fn parse_use( } } +pub fn parse_hide( + working_set: &mut StateWorkingSet, + spans: &[Span], +) -> (Statement, Option) { + let mut error = None; + let bytes = working_set.get_span_contents(spans[0]); + + if bytes == b"hide" && spans.len() >= 2 { + let (name_expr, err) = parse_string(working_set, spans[1]); + error = error.or(err); + + let name_bytes: Vec = working_set.get_span_contents(spans[1]).into(); + + // TODO: Do the import pattern stuff for bulk-hiding + + if working_set.hide_decl(&name_bytes).is_none() { + error = error.or_else(|| Some(ParseError::UnknownCommand(spans[1]))); + } + + // Create the Hide command call + let hide_decl_id = working_set + .find_decl(b"hide") + .expect("internal error: missing hide command"); + + let call = Box::new(Call { + head: spans[0], + decl_id: hide_decl_id, + positional: vec![name_expr], + named: vec![], + }); + + ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: span(spans), + ty: Type::Unknown, + custom_completion: None, + }])), + error, + ) + } else { + ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "Expected structure: hide ".into(), + span(spans), + )), + ) + } +} + pub fn parse_let( working_set: &mut StateWorkingSet, spans: &[Span], diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index db973c46c..fa9703a73 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -14,7 +14,7 @@ use nu_protocol::{ }; use crate::parse_keywords::{ - parse_alias, parse_def, parse_def_predecl, parse_let, parse_module, parse_use, + parse_alias, parse_def, parse_def_predecl, parse_hide, parse_let, parse_module, parse_use, }; #[derive(Debug, Clone)] @@ -2619,6 +2619,11 @@ pub fn parse_statement( b"alias" => parse_alias(working_set, spans), b"module" => parse_module(working_set, spans), b"use" => parse_use(working_set, spans), + b"export" => ( + garbage_statement(spans), + Some(ParseError::UnexpectedKeyword("export".into(), spans[0])), + ), + b"hide" => parse_hide(working_set, spans), _ => { let (expr, err) = parse_expression(working_set, spans); (Statement::Pipeline(Pipeline::from_vec(vec![expr])), err) @@ -2635,16 +2640,18 @@ pub fn parse_block( working_set.enter_scope(); } + let mut error = None; + // Pre-declare any definition so that definitions // that share the same block can see each other for pipeline in &lite_block.block { if pipeline.commands.len() == 1 { - parse_def_predecl(working_set, &pipeline.commands[0].parts); + if let Some(err) = parse_def_predecl(working_set, &pipeline.commands[0].parts) { + error = error.or(Some(err)); + } } } - let mut error = None; - let block: Block = lite_block .block .iter() diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index f0920ed5c..bbe6bdb1c 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -1,7 +1,10 @@ use super::Command; use crate::{ast::Block, BlockId, DeclId, Signature, Span, Type, VarId}; use core::panic; -use std::{collections::HashMap, slice::Iter}; +use std::{ + collections::{HashMap, HashSet}, + slice::Iter, +}; pub struct EngineState { files: Vec<(String, usize, usize)>, @@ -18,6 +21,7 @@ pub struct ScopeFrame { decls: HashMap, DeclId>, aliases: HashMap, Vec>, modules: HashMap, BlockId>, + hiding: HashSet, } impl ScopeFrame { @@ -27,6 +31,7 @@ impl ScopeFrame { decls: HashMap::new(), aliases: HashMap::new(), modules: HashMap::new(), + hiding: HashSet::new(), } } @@ -81,6 +86,9 @@ impl EngineState { for item in first.modules.into_iter() { last.modules.insert(item.0, item.1); } + for item in first.hiding.into_iter() { + last.hiding.insert(item); + } } } @@ -124,9 +132,15 @@ impl EngineState { } pub fn find_decl(&self, name: &[u8]) -> Option { + let mut hiding: HashSet = HashSet::new(); + for scope in self.scope.iter().rev() { + hiding.extend(&scope.hiding); + if let Some(decl_id) = scope.decls.get(name) { - return Some(*decl_id); + if !hiding.contains(decl_id) { + return Some(*decl_id); + } } } @@ -238,9 +252,10 @@ pub struct StateWorkingSet<'a> { pub struct StateDelta { files: Vec<(String, usize, usize)>, pub(crate) file_contents: Vec, - vars: Vec, // indexed by VarId - decls: Vec>, // indexed by DeclId - blocks: Vec, // indexed by BlockId + vars: Vec, // indexed by VarId + decls: Vec>, // indexed by DeclId + blocks: Vec, // indexed by BlockId + predecls: HashMap, DeclId>, // this should get erased after every def call pub scope: Vec, } @@ -274,6 +289,7 @@ impl<'a> StateWorkingSet<'a> { file_contents: vec![], vars: vec![], decls: vec![], + predecls: HashMap::new(), blocks: vec![], scope: vec![ScopeFrame::new()], }, @@ -304,11 +320,71 @@ impl<'a> StateWorkingSet<'a> { .scope .last_mut() .expect("internal error: missing required scope frame"); + scope_frame.decls.insert(name, decl_id); decl_id } + pub fn add_predecl(&mut self, decl: Box) -> Option { + let name = decl.name().as_bytes().to_vec(); + + self.delta.decls.push(decl); + let decl_id = self.num_decls() - 1; + + self.delta.predecls.insert(name, decl_id) + } + + pub fn merge_predecl(&mut self, name: &[u8]) -> Option { + if let Some(decl_id) = self.delta.predecls.remove(name) { + let scope_frame = self + .delta + .scope + .last_mut() + .expect("internal error: missing required scope frame"); + + scope_frame.decls.insert(name.into(), decl_id); + + return Some(decl_id); + } + + None + } + + pub fn hide_decl(&mut self, name: &[u8]) -> Option { + let mut hiding: HashSet = HashSet::new(); + + // Since we can mutate scope frames in delta, remove the id directly + for scope in self.delta.scope.iter_mut().rev() { + hiding.extend(&scope.hiding); + + if let Some(decl_id) = scope.decls.remove(name) { + return Some(decl_id); + } + } + + // We cannot mutate the permanent state => store the information in the current scope frame + let last_scope_frame = self + .delta + .scope + .last_mut() + .expect("internal error: missing required scope frame"); + + for scope in self.permanent_state.scope.iter().rev() { + hiding.extend(&scope.hiding); + + if let Some(decl_id) = scope.decls.get(name) { + if !hiding.contains(decl_id) { + // Do not hide already hidden decl + last_scope_frame.hiding.insert(*decl_id); + return Some(*decl_id); + } + } + } + + None + } + pub fn add_block(&mut self, block: Block) -> BlockId { self.delta.blocks.push(block); @@ -416,15 +492,27 @@ impl<'a> StateWorkingSet<'a> { } pub fn find_decl(&self, name: &[u8]) -> Option { + let mut hiding: HashSet = HashSet::new(); + + if let Some(decl_id) = self.delta.predecls.get(name) { + return Some(*decl_id); + } + for scope in self.delta.scope.iter().rev() { + hiding.extend(&scope.hiding); + if let Some(decl_id) = scope.decls.get(name) { return Some(*decl_id); } } for scope in self.permanent_state.scope.iter().rev() { + hiding.extend(&scope.hiding); + if let Some(decl_id) = scope.decls.get(name) { - return Some(*decl_id); + if !hiding.contains(decl_id) { + return Some(*decl_id); + } } } diff --git a/src/tests.rs b/src/tests.rs index 3ac7dc681..7416aecb6 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -346,7 +346,7 @@ fn better_block_types() -> TestResult { #[test] fn module_imports_1() -> TestResult { run_test( - r#"module foo { def a [] { 1 }; def b [] { 2 } }; use foo; foo.a"#, + r#"module foo { export def a [] { 1 }; def b [] { 2 } }; use foo; foo.a"#, "1", ) } @@ -354,7 +354,7 @@ fn module_imports_1() -> TestResult { #[test] fn module_imports_2() -> TestResult { run_test( - r#"module foo { def a [] { 1 }; def b [] { 2 } }; use foo.a; a"#, + r#"module foo { export def a [] { 1 }; def b [] { 2 } }; use foo.a; a"#, "1", ) } @@ -362,7 +362,7 @@ fn module_imports_2() -> TestResult { #[test] fn module_imports_3() -> TestResult { run_test( - r#"module foo { def a [] { 1 }; def b [] { 2 } }; use foo.*; b"#, + r#"module foo { export def a [] { 1 }; export def b [] { 2 } }; use foo.*; b"#, "2", ) } @@ -370,7 +370,7 @@ fn module_imports_3() -> TestResult { #[test] fn module_imports_4() -> TestResult { fail_test( - r#"module foo { def a [] { 1 }; def b [] { 2 } }; use foo.c"#, + r#"module foo { export def a [] { 1 }; export def b [] { 2 } }; use foo.c"#, "not find import", ) } @@ -378,11 +378,77 @@ fn module_imports_4() -> TestResult { #[test] fn module_imports_5() -> TestResult { run_test( - r#"module foo { def a [] { 1 }; def b [] { 2 }; def c [] { 3 } }; use foo.[a, c]; c"#, + r#"module foo { export def a [] { 1 }; def b [] { 2 }; export def c [] { 3 } }; use foo.[a, c]; c"#, "3", ) } +#[test] +fn module_import_uses_internal_command() -> TestResult { + run_test( + r#"module foo { def b [] { 2 }; export def a [] { b } }; use foo; foo.a"#, + "2", + ) +} + +#[test] +fn hides_def() -> TestResult { + fail_test(r#"def foo [] { "foo" }; hide foo; foo"#, "not found") +} + +#[test] +fn hides_def_then_redefines() -> TestResult { + fail_test( + r#"def foo [] { "foo" }; hide foo; def foo [] { "bar" }; foo"#, + "defined more than once", + ) +} + +#[test] +fn hides_def_in_scope_1() -> TestResult { + fail_test(r#"def foo [] { "foo" }; do { hide foo; foo }"#, "not found") +} + +#[test] +fn hides_def_in_scope_2() -> TestResult { + run_test( + r#"def foo [] { "foo" }; do { def foo [] { "bar" }; hide foo; foo }"#, + "foo", + ) +} + +#[test] +fn hides_def_in_scope_3() -> TestResult { + fail_test( + r#"def foo [] { "foo" }; do { hide foo; def foo [] { "bar" }; hide foo; foo }"#, + "not found", + ) +} + +#[test] +fn hides_def_in_scope_4() -> TestResult { + fail_test( + r#"def foo [] { "foo" }; do { def foo [] { "bar" }; hide foo; hide foo; foo }"#, + "not found", + ) +} + +#[test] +fn hide_twice_not_allowed() -> TestResult { + fail_test( + r#"def foo [] { "foo" }; hide foo; hide foo"#, + "unknown command", + ) +} + +#[test] +fn def_twice_should_fail() -> TestResult { + fail_test( + r#"def foo [] { "foo" }; def foo [] { "bar" }"#, + "defined more than once", + ) +} + #[test] fn from_json_1() -> TestResult { run_test(r#"('{"name": "Fred"}' | from json).name"#, "Fred")