From 3a2c7900d6b1306282bf69967c75b5333b7452b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BD=C3=A1dn=C3=ADk?= Date: Thu, 22 Dec 2022 00:21:03 +0200 Subject: [PATCH] Initial support for parse-time constants (#7436) --- crates/nu-command/src/core_commands/const_.rs | 104 ++++ crates/nu-command/src/core_commands/let_.rs | 3 +- crates/nu-command/src/core_commands/mod.rs | 2 + .../src/core_commands/overlay/use_.rs | 21 +- crates/nu-command/src/default_context.rs | 1 + .../nu-command/tests/commands/source_env.rs | 22 + crates/nu-parser/src/errors.rs | 14 + crates/nu-parser/src/eval.rs | 124 +++++ crates/nu-parser/src/lib.rs | 1 + crates/nu-parser/src/parse_keywords.rs | 450 ++++++++++-------- crates/nu-parser/src/parser.rs | 48 +- crates/nu-protocol/src/engine/engine_state.rs | 36 ++ crates/nu-protocol/src/engine/overlay.rs | 4 +- tests/const_/mod.rs | 103 ++++ tests/main.rs | 1 + tests/modules/mod.rs | 41 ++ tests/overlays/mod.rs | 44 ++ tests/parsing/mod.rs | 11 + 18 files changed, 788 insertions(+), 242 deletions(-) create mode 100644 crates/nu-command/src/core_commands/const_.rs create mode 100644 crates/nu-parser/src/eval.rs create mode 100644 tests/const_/mod.rs diff --git a/crates/nu-command/src/core_commands/const_.rs b/crates/nu-command/src/core_commands/const_.rs new file mode 100644 index 000000000..ef13c3a9e --- /dev/null +++ b/crates/nu-command/src/core_commands/const_.rs @@ -0,0 +1,104 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type}; + +#[derive(Clone)] +pub struct Const; + +impl Command for Const { + fn name(&self) -> &str { + "const" + } + + fn usage(&self) -> &str { + "Create a parse-time constant." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("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::Expression)), + "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 search_terms(&self) -> Vec<&str> { + vec!["set", "let"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let var_id = call + .positional_nth(0) + .expect("checked through parser") + .as_var() + .expect("internal error: missing variable"); + + if let Some(constval) = engine_state.find_constant(var_id, &[]) { + // Instead of creating a second copy of the value in the stack, we could change + // stack.get_var() to check engine_state.find_constant(). + stack.add_var(var_id, constval.clone()); + + Ok(PipelineData::empty()) + } else { + Err(ShellError::NushellFailedSpanned( + "Missing Constant".to_string(), + "constant not added by the parser".to_string(), + call.head, + )) + } + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Create a new parse-time constant.", + example: "let x = 10", + result: None, + }, + Example { + description: "Create a composite constant value", + example: "let x = { a: 10, b: 20 }", + result: None, + }, + ] + } +} + +#[cfg(test)] +mod test { + use nu_protocol::engine::CommandType; + + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Const {}) + } + + #[test] + fn test_command_type() { + assert!(matches!(Const.command_type(), CommandType::Keyword)); + } +} diff --git a/crates/nu-command/src/core_commands/let_.rs b/crates/nu-command/src/core_commands/let_.rs index 41f98d0f0..4818ac70a 100644 --- a/crates/nu-command/src/core_commands/let_.rs +++ b/crates/nu-command/src/core_commands/let_.rs @@ -70,9 +70,8 @@ impl Command for Let { )? .0; - //println!("Adding: {:?} to {}", rhs, var_id); - stack.add_var(var_id, rhs.into_value(call.head)); + Ok(PipelineData::empty()) } diff --git a/crates/nu-command/src/core_commands/mod.rs b/crates/nu-command/src/core_commands/mod.rs index 8cde57223..e8ce32bde 100644 --- a/crates/nu-command/src/core_commands/mod.rs +++ b/crates/nu-command/src/core_commands/mod.rs @@ -2,6 +2,7 @@ mod alias; mod ast; mod break_; mod commandline; +mod const_; mod continue_; mod debug; mod def; @@ -40,6 +41,7 @@ pub use alias::Alias; pub use ast::Ast; pub use break_::Break; pub use commandline::Commandline; +pub use const_::Const; pub use continue_::Continue; pub use debug::Debug; pub use def::Def; diff --git a/crates/nu-command/src/core_commands/overlay/use_.rs b/crates/nu-command/src/core_commands/overlay/use_.rs index dfa92c2be..8b409331a 100644 --- a/crates/nu-command/src/core_commands/overlay/use_.rs +++ b/crates/nu-command/src/core_commands/overlay/use_.rs @@ -84,25 +84,8 @@ impl Command for OverlayUse { )); }; - let overlay_name = if let Some(kw_expression) = call.positional_nth(1) { - // If renamed via the 'as' keyword, use the new name as the overlay name - if let Some(new_name_expression) = kw_expression.as_keyword() { - if let Some(new_name) = new_name_expression.as_string() { - new_name - } else { - return Err(ShellError::NushellFailedSpanned( - "Wrong keyword type".to_string(), - "keyword argument not a string".to_string(), - new_name_expression.span, - )); - } - } else { - return Err(ShellError::NushellFailedSpanned( - "Wrong keyword type".to_string(), - "keyword argument not a keyword".to_string(), - kw_expression.span, - )); - } + let overlay_name = if let Some(name) = call.opt(engine_state, caller_stack, 1)? { + name } else if engine_state .find_overlay(name_arg.item.as_bytes()) .is_some() diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 4249a6c6b..ec41319fe 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -32,6 +32,7 @@ pub fn create_default_context() -> EngineState { Ast, Break, Commandline, + Const, Continue, Debug, Def, diff --git a/crates/nu-command/tests/commands/source_env.rs b/crates/nu-command/tests/commands/source_env.rs index 733a610cd..aa7ae739b 100644 --- a/crates/nu-command/tests/commands/source_env.rs +++ b/crates/nu-command/tests/commands/source_env.rs @@ -293,3 +293,25 @@ fn source_env_is_scoped() { assert!(actual.err.contains("executable was not found")); }) } + +#[test] +fn source_env_const_file() { + Playground::setup("source_env_const_file", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "spam.nu", + r#" + let-env FOO = 'foo' + "#, + )]); + + let inp = &[ + r#"const file = 'spam.nu'"#, + r#"source-env $file"#, + r#"$env.FOO"#, + ]; + + let actual = nu!(cwd: dirs.test(), pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "foo"); + }) +} diff --git a/crates/nu-parser/src/errors.rs b/crates/nu-parser/src/errors.rs index cdfdb0441..d9ea6565b 100644 --- a/crates/nu-parser/src/errors.rs +++ b/crates/nu-parser/src/errors.rs @@ -378,6 +378,19 @@ pub enum ParseError { #[diagnostic(code(nu::shell::error_reading_file), url(docsrs))] ReadingFile(String, #[label("{0}")] Span), + /// Tried assigning non-constant value to a constant + /// + /// ## Resolution + /// + /// Only a subset of expressions are allowed to be assigned as a constant during parsing. + #[error("Not a constant.")] + #[diagnostic( + code(nu::parser::not_a_constant), + url(docsrs), + help("Only a subset of expressions are allowed constants during parsing. Try using the 'let' command or typing the value literally.") + )] + NotAConstant(#[label = "Value is not a parse-time constant"] Span), + #[error("{0}")] #[diagnostic()] LabeledError(String, String, #[label("{1}")] Span), @@ -450,6 +463,7 @@ impl ParseError { ParseError::ShellErrRedirect(s) => *s, ParseError::ShellOutErrRedirect(s) => *s, ParseError::UnknownOperator(_, _, s) => *s, + ParseError::NotAConstant(s) => *s, } } } diff --git a/crates/nu-parser/src/eval.rs b/crates/nu-parser/src/eval.rs new file mode 100644 index 000000000..40bfca575 --- /dev/null +++ b/crates/nu-parser/src/eval.rs @@ -0,0 +1,124 @@ +use crate::ParseError; +use nu_protocol::{ + ast::{Expr, Expression}, + engine::StateWorkingSet, + Span, Value, +}; + +/// Evaluate a constant value at parse time +/// +/// Based off eval_expression() in the engine +pub fn eval_constant( + working_set: &StateWorkingSet, + expr: &Expression, +) -> Result { + match &expr.expr { + Expr::Bool(b) => Ok(Value::boolean(*b, expr.span)), + Expr::Int(i) => Ok(Value::int(*i, expr.span)), + Expr::Float(f) => Ok(Value::float(*f, expr.span)), + Expr::Binary(b) => Ok(Value::Binary { + val: b.clone(), + span: expr.span, + }), + Expr::Var(var_id) => match working_set.find_constant(*var_id) { + Some(val) => Ok(val.clone()), + None => Err(ParseError::NotAConstant(expr.span)), + }, + Expr::CellPath(cell_path) => Ok(Value::CellPath { + val: cell_path.clone(), + span: expr.span, + }), + Expr::FullCellPath(cell_path) => { + let value = eval_constant(working_set, &cell_path.head)?; + + match value.follow_cell_path(&cell_path.tail, false) { + Ok(val) => Ok(val), + // TODO: Better error conversion + Err(shell_error) => Err(ParseError::LabeledError( + "Error when following cell path".to_string(), + format!("{:?}", shell_error), + expr.span, + )), + } + } + Expr::DateTime(dt) => Ok(Value::Date { + val: *dt, + span: expr.span, + }), + Expr::List(x) => { + let mut output = vec![]; + for expr in x { + output.push(eval_constant(working_set, expr)?); + } + Ok(Value::List { + vals: output, + span: expr.span, + }) + } + Expr::Record(fields) => { + let mut cols = vec![]; + let mut vals = vec![]; + for (col, val) in fields { + // avoid duplicate cols. + let col_name = value_as_string(eval_constant(working_set, col)?, expr.span)?; + let pos = cols.iter().position(|c| c == &col_name); + match pos { + Some(index) => { + vals[index] = eval_constant(working_set, val)?; + } + None => { + cols.push(col_name); + vals.push(eval_constant(working_set, val)?); + } + } + } + + Ok(Value::Record { + cols, + vals, + span: expr.span, + }) + } + Expr::Table(headers, vals) => { + let mut output_headers = vec![]; + for expr in headers { + output_headers.push(value_as_string( + eval_constant(working_set, expr)?, + expr.span, + )?); + } + + let mut output_rows = vec![]; + for val in vals { + let mut row = vec![]; + for expr in val { + row.push(eval_constant(working_set, expr)?); + } + output_rows.push(Value::Record { + cols: output_headers.clone(), + vals: row, + span: expr.span, + }); + } + Ok(Value::List { + vals: output_rows, + span: expr.span, + }) + } + Expr::Keyword(_, _, expr) => eval_constant(working_set, expr), + Expr::String(s) => Ok(Value::String { + val: s.clone(), + span: expr.span, + }), + Expr::Nothing => Ok(Value::Nothing { span: expr.span }), + _ => Err(ParseError::NotAConstant(expr.span)), + } +} + +/// Get the value as a string +pub fn value_as_string(value: Value, span: Span) -> Result { + match value { + Value::String { val, .. } => Ok(val), + _ => Err(ParseError::NotAConstant(span)), + } +} diff --git a/crates/nu-parser/src/lib.rs b/crates/nu-parser/src/lib.rs index 66f56b70d..f5a69fad7 100644 --- a/crates/nu-parser/src/lib.rs +++ b/crates/nu-parser/src/lib.rs @@ -1,5 +1,6 @@ mod deparse; mod errors; +mod eval; mod flatten; mod known_external; mod lex; diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index 2241b13c6..b3b49d727 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -1,3 +1,4 @@ +use crate::eval::{eval_constant, value_as_string}; use log::trace; use nu_path::canonicalize_with; use nu_protocol::{ @@ -20,8 +21,8 @@ use crate::{ lex, parser::{ check_call, check_name, garbage, garbage_pipeline, lite_parse, parse, parse_internal_call, - parse_multispan_value, parse_signature, parse_string, parse_var_with_opt_type, trim_quotes, - LiteCommand, LiteElement, ParsedInternalCall, + parse_multispan_value, parse_signature, parse_string, parse_value, parse_var_with_opt_type, + trim_quotes, LiteCommand, LiteElement, ParsedInternalCall, }, unescape_unquote_string, ParseError, }; @@ -1636,144 +1637,143 @@ pub fn parse_use( // TODO: Add checking for importing too long import patterns, e.g.: // > use spam foo non existent names here do not throw error - let (import_pattern, module) = - if let Some(module_id) = working_set.find_module(&import_pattern.head.name) { - (import_pattern, working_set.get_module(module_id).clone()) - } else { - // It could be a file - // TODO: Do not close over when loading module from file? + let (import_pattern, module) = if let Some(module_id) = import_pattern.head.id { + (import_pattern, working_set.get_module(module_id).clone()) + } else { + // It could be a file + // TODO: Do not close over when loading module from file? - let (module_filename, err) = - unescape_unquote_string(&import_pattern.head.name, import_pattern.head.span); + let (module_filename, err) = + unescape_unquote_string(&import_pattern.head.name, import_pattern.head.span); - if err.is_none() { - if let Some(module_path) = - find_in_dirs(&module_filename, working_set, &cwd, LIB_DIRS_ENV) + if err.is_none() { + if let Some(module_path) = + find_in_dirs(&module_filename, working_set, &cwd, LIB_DIRS_ENV) + { + if let Some(i) = working_set + .parsed_module_files + .iter() + .rposition(|p| p == &module_path) { - if let Some(i) = working_set + let mut files: Vec = working_set .parsed_module_files + .split_off(i) .iter() - .rposition(|p| p == &module_path) - { - let mut files: Vec = working_set - .parsed_module_files - .split_off(i) - .iter() - .map(|p| p.to_string_lossy().to_string()) - .collect(); + .map(|p| p.to_string_lossy().to_string()) + .collect(); - files.push(module_path.to_string_lossy().to_string()); + files.push(module_path.to_string_lossy().to_string()); - let msg = files.join("\nuses "); + let msg = files.join("\nuses "); - return ( - Pipeline::from_vec(vec![Expression { - expr: Expr::Call(call), - span: call_span, - ty: Type::Any, - custom_completion: None, - }]), - vec![], - Some(ParseError::CyclicalModuleImport( - msg, - import_pattern.head.span, - )), - ); - } + return ( + Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Any, + custom_completion: None, + }]), + vec![], + Some(ParseError::CyclicalModuleImport( + msg, + import_pattern.head.span, + )), + ); + } - let module_name = if let Some(stem) = module_path.file_stem() { - stem.to_string_lossy().to_string() + let module_name = if let Some(stem) = module_path.file_stem() { + stem.to_string_lossy().to_string() + } else { + return ( + Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Any, + custom_completion: None, + }]), + vec![], + Some(ParseError::ModuleNotFound(import_pattern.head.span)), + ); + }; + + if let Ok(contents) = std::fs::read(&module_path) { + let span_start = working_set.next_span_start(); + working_set.add_file(module_filename, &contents); + let span_end = working_set.next_span_start(); + + // Change the currently parsed directory + let prev_currently_parsed_cwd = if let Some(parent) = module_path.parent() { + let prev = working_set.currently_parsed_cwd.clone(); + + working_set.currently_parsed_cwd = Some(parent.into()); + + prev } else { - return ( - Pipeline::from_vec(vec![Expression { - expr: Expr::Call(call), - span: call_span, - ty: Type::Any, - custom_completion: None, - }]), - vec![], - Some(ParseError::ModuleNotFound(import_pattern.head.span)), - ); + working_set.currently_parsed_cwd.clone() }; - if let Ok(contents) = std::fs::read(&module_path) { - let span_start = working_set.next_span_start(); - working_set.add_file(module_filename, &contents); - let span_end = working_set.next_span_start(); + // Add the file to the stack of parsed module files + working_set.parsed_module_files.push(module_path); - // Change the currently parsed directory - let prev_currently_parsed_cwd = if let Some(parent) = module_path.parent() { - let prev = working_set.currently_parsed_cwd.clone(); + // Parse the module + let (block, module, err) = parse_module_block( + working_set, + Span::new(span_start, span_end), + expand_aliases_denylist, + ); + error = error.or(err); - working_set.currently_parsed_cwd = Some(parent.into()); + // Remove the file from the stack of parsed module files + working_set.parsed_module_files.pop(); - prev - } else { - working_set.currently_parsed_cwd.clone() - }; + // Restore the currently parsed directory back + working_set.currently_parsed_cwd = prev_currently_parsed_cwd; - // Add the file to the stack of parsed module files - working_set.parsed_module_files.push(module_path); + let _ = working_set.add_block(block); + let module_id = working_set.add_module(&module_name, module.clone()); - // Parse the module - let (block, module, err) = parse_module_block( - working_set, - Span::new(span_start, span_end), - expand_aliases_denylist, - ); - error = error.or(err); - - // Remove the file from the stack of parsed module files - working_set.parsed_module_files.pop(); - - // Restore the currently parsed directory back - working_set.currently_parsed_cwd = prev_currently_parsed_cwd; - - let _ = working_set.add_block(block); - let module_id = working_set.add_module(&module_name, module.clone()); - - ( - ImportPattern { - head: ImportPatternHead { - name: module_name.into(), - id: Some(module_id), - span: import_pattern.head.span, - }, - members: import_pattern.members, - hidden: HashSet::new(), + ( + ImportPattern { + head: ImportPatternHead { + name: module_name.into(), + id: Some(module_id), + span: import_pattern.head.span, }, - module, - ) - } else { - return ( - Pipeline::from_vec(vec![Expression { - expr: Expr::Call(call), - span: call_span, - ty: Type::Any, - custom_completion: None, - }]), - vec![], - Some(ParseError::ModuleNotFound(import_pattern.head.span)), - ); - } + members: import_pattern.members, + hidden: HashSet::new(), + }, + module, + ) } else { - error = error.or(Some(ParseError::ModuleNotFound(import_pattern.head.span))); - - let old_span = import_pattern.head.span; - - let mut import_pattern = ImportPattern::new(); - import_pattern.head.span = old_span; - - (import_pattern, Module::new()) + return ( + Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Any, + custom_completion: None, + }]), + vec![], + Some(ParseError::ModuleNotFound(import_pattern.head.span)), + ); } } else { - return ( - garbage_pipeline(spans), - vec![], - Some(ParseError::NonUtf8(import_pattern.head.span)), - ); + error = error.or(Some(ParseError::ModuleNotFound(import_pattern.head.span))); + + let old_span = import_pattern.head.span; + + let mut import_pattern = ImportPattern::new(); + import_pattern.head.span = old_span; + + (import_pattern, Module::new()) } - }; + } else { + return ( + garbage_pipeline(spans), + vec![], + Some(ParseError::NonUtf8(import_pattern.head.span)), + ); + } + }; let (decls_to_use, aliases_to_use) = if import_pattern.members.is_empty() { ( @@ -2386,16 +2386,16 @@ pub fn parse_overlay_use( }; let (overlay_name, overlay_name_span) = if let Some(expr) = call.positional_nth(0) { - if let Some(s) = expr.as_string() { - (s, expr.span) - } else { - return ( - garbage_pipeline(spans), - Some(ParseError::UnknownState( - "internal error: Module name not a string".into(), - expr.span, - )), - ); + match eval_constant(working_set, expr) { + Ok(val) => match value_as_string(val, expr.span) { + Ok(s) => (s, expr.span), + Err(err) => { + return (garbage_pipeline(spans), Some(err)); + } + }, + Err(err) => { + return (garbage_pipeline(spans), Some(err)); + } } } else { return ( @@ -2409,20 +2409,15 @@ pub fn parse_overlay_use( let new_name = if let Some(kw_expression) = call.positional_nth(1) { if let Some(new_name_expression) = kw_expression.as_keyword() { - if let Some(new_name) = new_name_expression.as_string() { - Some(Spanned { - item: new_name, - span: new_name_expression.span, - }) - } else { - return ( - garbage_pipeline(spans), - Some(ParseError::TypeMismatch( - Type::String, - new_name_expression.ty.clone(), - new_name_expression.span, - )), - ); + match eval_constant(working_set, new_name_expression) { + Ok(val) => match value_as_string(val, new_name_expression.span) { + Ok(s) => Some(Spanned { + item: s, + span: new_name_expression.span, + }), + Err(err) => return (garbage_pipeline(spans), Some(err)), + }, + Err(err) => return (garbage_pipeline(spans), Some(err)), } } else { return ( @@ -2751,19 +2746,23 @@ pub fn parse_overlay_hide( (pipeline, None) } -pub fn parse_let( +pub fn parse_let_or_const( working_set: &mut StateWorkingSet, spans: &[Span], expand_aliases_denylist: &[usize], ) -> (Pipeline, Option) { let name = working_set.get_span_contents(spans[0]); - if name == b"let" { + if name == b"let" || name == b"const" { + let is_const = &name == b"const"; + if let Some((span, err)) = check_name(working_set, spans) { return (Pipeline::from_vec(vec![garbage(*span)]), Some(err)); } - if let Some(decl_id) = working_set.find_decl(b"let", &Type::Any) { + if let Some(decl_id) = + working_set.find_decl(if is_const { b"const" } else { b"let" }, &Type::Any) + { let cmd = working_set.get_decl(decl_id); let call_signature = cmd.signature().call_signature(); @@ -2815,6 +2814,15 @@ pub fn parse_let( if let Some(var_id) = var_id { working_set.set_variable_type(var_id, rhs_type); + + if is_const { + match eval_constant(working_set, &rvalue) { + Ok(val) => { + working_set.add_constant(var_id, val); + } + Err(err) => error = error.or(Some(err)), + } + } } let call = Box::new(Call { @@ -2866,7 +2874,7 @@ pub fn parse_let( ( garbage_pipeline(spans), Some(ParseError::UnknownState( - "internal error: let statement unparseable".into(), + "internal error: let or const statement unparseable".into(), span(spans), )), ) @@ -3036,79 +3044,111 @@ pub fn parse_source( // Command and one file name if spans.len() >= 2 { - let name_expr = working_set.get_span_contents(spans[1]); - let (filename, err) = unescape_unquote_string(name_expr, spans[1]); + let (expr, err) = parse_value( + working_set, + spans[1], + &SyntaxShape::Any, + expand_aliases_denylist, + ); - if err.is_none() { - if let Some(path) = find_in_dirs(&filename, working_set, &cwd, LIB_DIRS_ENV) { - if let Ok(contents) = std::fs::read(&path) { - // Change currently parsed directory - let prev_currently_parsed_cwd = if let Some(parent) = path.parent() { - let prev = working_set.currently_parsed_cwd.clone(); + error = error.or(err); - working_set.currently_parsed_cwd = Some(parent.into()); + let val = match eval_constant(working_set, &expr) { + Ok(val) => val, + Err(err) => { + return ( + Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: span(&spans[1..]), + ty: Type::Any, + custom_completion: None, + }]), + Some(err), + ); + } + }; - prev - } else { - working_set.currently_parsed_cwd.clone() - }; + let filename = match value_as_string(val, spans[1]) { + Ok(s) => s, + Err(err) => { + return ( + Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: span(&spans[1..]), + ty: Type::Any, + custom_completion: None, + }]), + Some(err), + ); + } + }; - // This will load the defs from the file into the - // working set, if it was a successful parse. - let (block, err) = parse( - working_set, - path.file_name().and_then(|x| x.to_str()), - &contents, - scoped, - expand_aliases_denylist, - ); + if let Some(path) = find_in_dirs(&filename, working_set, &cwd, LIB_DIRS_ENV) { + if let Ok(contents) = std::fs::read(&path) { + // Change currently parsed directory + let prev_currently_parsed_cwd = if let Some(parent) = path.parent() { + let prev = working_set.currently_parsed_cwd.clone(); - // Restore the currently parsed directory back - working_set.currently_parsed_cwd = prev_currently_parsed_cwd; + working_set.currently_parsed_cwd = Some(parent.into()); - if err.is_some() { - // Unsuccessful parse of file - return ( - Pipeline::from_vec(vec![Expression { - expr: Expr::Call(call), - span: span(&spans[1..]), - ty: Type::Any, - custom_completion: None, - }]), - // Return the file parse error - err, - ); - } else { - // Save the block into the working set - let block_id = working_set.add_block(block); + prev + } else { + working_set.currently_parsed_cwd.clone() + }; - let mut call_with_block = call; + // This will load the defs from the file into the + // working set, if it was a successful parse. + let (block, err) = parse( + working_set, + path.file_name().and_then(|x| x.to_str()), + &contents, + scoped, + expand_aliases_denylist, + ); - // FIXME: Adding this expression to the positional creates a syntax highlighting error - // after writing `source example.nu` - call_with_block.add_positional(Expression { - expr: Expr::Int(block_id as i64), - span: spans[1], + // Restore the currently parsed directory back + working_set.currently_parsed_cwd = prev_currently_parsed_cwd; + + if err.is_some() { + // Unsuccessful parse of file + return ( + Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: span(&spans[1..]), ty: Type::Any, custom_completion: None, - }); + }]), + // Return the file parse error + err, + ); + } else { + // Save the block into the working set + let block_id = working_set.add_block(block); - return ( - Pipeline::from_vec(vec![Expression { - expr: Expr::Call(call_with_block), - span: span(spans), - ty: Type::Any, - custom_completion: None, - }]), - None, - ); - } + let mut call_with_block = call; + + // FIXME: Adding this expression to the positional creates a syntax highlighting error + // after writing `source example.nu` + call_with_block.add_positional(Expression { + expr: Expr::Int(block_id as i64), + span: spans[1], + ty: Type::Any, + custom_completion: None, + }); + + return ( + Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call_with_block), + span: span(spans), + ty: Type::Any, + custom_completion: None, + }]), + None, + ); } - } else { - error = error.or(Some(ParseError::SourcedFileNotFound(filename, spans[1]))); } } else { - return (garbage_pipeline(spans), Some(ParseError::NonUtf8(spans[1]))); + error = error.or(Some(ParseError::SourcedFileNotFound(filename, spans[1]))); } } return ( diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index d99360058..711a29560 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -1,4 +1,5 @@ use crate::{ + eval::{eval_constant, value_as_string}, lex, parse_mut, type_check::{math_result_type, type_compatible}, ParseError, Token, TokenContents, @@ -17,8 +18,8 @@ use nu_protocol::{ use crate::parse_keywords::{ parse_alias, parse_def, parse_def_predecl, parse_export_in_block, parse_extern, parse_for, - parse_hide, parse_let, parse_module, parse_overlay, parse_source, parse_use, parse_where, - parse_where_expr, + parse_hide, parse_let_or_const, parse_module, parse_overlay, parse_source, parse_use, + parse_where, parse_where_expr, }; use itertools::Itertools; @@ -2832,11 +2833,8 @@ pub fn parse_import_pattern( ) -> (Expression, Option) { let mut error = None; - let (head, head_span) = if let Some(head_span) = spans.get(0) { - ( - working_set.get_span_contents(*head_span).to_vec(), - head_span, - ) + let head_span = if let Some(head_span) = spans.get(0) { + head_span } else { return ( garbage(span(spans)), @@ -2844,7 +2842,25 @@ pub fn parse_import_pattern( ); }; - let maybe_module_id = working_set.find_module(&head); + let (head_expr, err) = parse_value( + working_set, + *head_span, + &SyntaxShape::Any, + expand_aliases_denylist, + ); + error = error.or(err); + + let (maybe_module_id, head_name) = match eval_constant(working_set, &head_expr) { + Ok(val) => match value_as_string(val, head_expr.span) { + Ok(s) => (working_set.find_module(s.as_bytes()), s.into_bytes()), + Err(err) => { + return (garbage(span(spans)), error.or(Some(err))); + } + }, + Err(err) => { + return (garbage(span(spans)), error.or(Some(err))); + } + }; let (import_pattern, err) = if let Some(tail_span) = spans.get(1) { // FIXME: expand this to handle deeper imports once we support module imports @@ -2853,7 +2869,7 @@ pub fn parse_import_pattern( ( ImportPattern { head: ImportPatternHead { - name: head, + name: head_name, id: maybe_module_id, span: *head_span, }, @@ -2886,7 +2902,7 @@ pub fn parse_import_pattern( ( ImportPattern { head: ImportPatternHead { - name: head, + name: head_name, id: maybe_module_id, span: *head_span, }, @@ -2899,7 +2915,7 @@ pub fn parse_import_pattern( _ => ( ImportPattern { head: ImportPatternHead { - name: head, + name: head_name, id: maybe_module_id, span: *head_span, }, @@ -2914,7 +2930,7 @@ pub fn parse_import_pattern( ( ImportPattern { head: ImportPatternHead { - name: head, + name: head_name, id: maybe_module_id, span: *head_span, }, @@ -2931,7 +2947,7 @@ pub fn parse_import_pattern( ( ImportPattern { head: ImportPatternHead { - name: head, + name: head_name, id: maybe_module_id, span: *head_span, }, @@ -4898,7 +4914,7 @@ pub fn parse_expression( .0, Some(ParseError::BuiltinCommandInPipeline("for".into(), spans[0])), ), - b"let" => ( + b"let" | b"const" => ( parse_call( working_set, &spans[pos..], @@ -5166,7 +5182,9 @@ pub fn parse_builtin_commands( match name { b"def" | b"def-env" => parse_def(working_set, lite_command, expand_aliases_denylist), b"extern" => parse_extern(working_set, lite_command, expand_aliases_denylist), - b"let" => parse_let(working_set, &lite_command.parts, expand_aliases_denylist), + b"let" | b"const" => { + parse_let_or_const(working_set, &lite_command.parts, expand_aliases_denylist) + } b"mut" => parse_mut(working_set, &lite_command.parts, expand_aliases_denylist), b"for" => { let (expr, err) = parse_for(working_set, &lite_command.parts, expand_aliases_denylist); diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index cf2d68365..5dabda7e6 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -173,6 +173,9 @@ impl EngineState { for item in delta_overlay.vars.into_iter() { existing_overlay.vars.insert(item.0, item.1); } + for item in delta_overlay.constants.into_iter() { + existing_overlay.constants.insert(item.0, item.1); + } for item in delta_overlay.aliases.into_iter() { existing_overlay.aliases.insert(item.0, item.1); } @@ -626,6 +629,16 @@ impl EngineState { output } + pub fn find_constant(&self, var_id: VarId, removed_overlays: &[Vec]) -> Option<&Value> { + for overlay_frame in self.active_overlays(removed_overlays).iter().rev() { + if let Some(val) = overlay_frame.constants.get(&var_id) { + return Some(val); + } + } + + None + } + pub fn get_span_contents(&self, span: &Span) -> &[u8] { for (contents, start, finish) in &self.file_contents { if span.start >= *start && span.end <= *finish { @@ -1654,6 +1667,29 @@ impl<'a> StateWorkingSet<'a> { } } + pub fn add_constant(&mut self, var_id: VarId, val: Value) { + self.last_overlay_mut().constants.insert(var_id, val); + } + + pub fn find_constant(&self, var_id: VarId) -> Option<&Value> { + 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) + .iter() + .rev() + { + if let Some(val) = overlay_frame.constants.get(&var_id) { + return Some(val); + } + } + } + + self.permanent_state + .find_constant(var_id, &removed_overlays) + } + pub fn get_variable(&self, var_id: VarId) -> &Variable { let num_permanent_vars = self.permanent_state.num_vars(); if var_id < num_permanent_vars { diff --git a/crates/nu-protocol/src/engine/overlay.rs b/crates/nu-protocol/src/engine/overlay.rs index ca19796ce..c7a673630 100644 --- a/crates/nu-protocol/src/engine/overlay.rs +++ b/crates/nu-protocol/src/engine/overlay.rs @@ -1,4 +1,4 @@ -use crate::{AliasId, DeclId, ModuleId, OverlayId, Type, VarId}; +use crate::{AliasId, DeclId, ModuleId, OverlayId, Type, Value, VarId}; use std::borrow::Borrow; use std::collections::HashMap; use std::hash::{Hash, Hasher}; @@ -199,6 +199,7 @@ impl ScopeFrame { #[derive(Debug, Clone)] pub struct OverlayFrame { pub vars: HashMap, VarId>, + pub constants: HashMap, pub predecls: HashMap, DeclId>, // temporary storage for predeclarations pub decls: HashMap<(Vec, Type), DeclId>, pub aliases: HashMap, AliasId>, @@ -212,6 +213,7 @@ impl OverlayFrame { pub fn from_origin(origin: ModuleId, prefixed: bool) -> Self { Self { vars: HashMap::new(), + constants: HashMap::new(), predecls: HashMap::new(), decls: HashMap::new(), aliases: HashMap::new(), diff --git a/tests/const_/mod.rs b/tests/const_/mod.rs new file mode 100644 index 000000000..492257a41 --- /dev/null +++ b/tests/const_/mod.rs @@ -0,0 +1,103 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn const_bool() { + let inp = &[r#"const x = false"#, r#"$x"#]; + + let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "false"); +} + +#[test] +fn const_int() { + let inp = &[r#"const x = 10"#, r#"$x"#]; + + let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "10"); +} + +#[test] +fn const_float() { + let inp = &[r#"const x = 1.234"#, r#"$x"#]; + + let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "1.234"); +} + +#[test] +fn const_binary() { + let inp = &[r#"const x = 0x[12]"#, r#"$x"#]; + + let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; "))); + + assert!(actual.out.contains("12")); +} + +#[test] +fn const_datetime() { + let inp = &[r#"const x = 2021-02-27T13:55:40+00:00"#, r#"$x"#]; + + let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; "))); + + assert!(actual.out.contains("Sat, 27 Feb 2021 13:55:40")); +} + +#[test] +fn const_list() { + let inp = &[r#"const x = [ a b c ]"#, r#"$x | describe"#]; + + let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "list"); +} + +#[test] +fn const_record() { + let inp = &[r#"const x = { a: 10, b: 20, c: 30 }"#, r#"$x | describe"#]; + + let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "record"); +} + +#[test] +fn const_table() { + let inp = &[ + r#"const x = [[a b c]; [10 20 30] [100 200 300]]"#, + r#"$x | describe"#, + ]; + + let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "table"); +} + +#[test] +fn const_string() { + let inp = &[r#"const x = "abc""#, r#"$x"#]; + + let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "abc"); +} + +#[test] +fn const_nothing() { + let inp = &[r#"const x = $nothing"#, r#"$x | describe"#]; + + let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "nothing"); +} + +#[test] +fn const_unsupported() { + let inp = &[r#"const x = ('abc' | str length)"#]; + + let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; "))); + + assert!(actual.err.contains("not_a_constant")); +} diff --git a/tests/main.rs b/tests/main.rs index 161292bd9..ce3150811 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -1,5 +1,6 @@ extern crate nu_test_support; +mod const_; mod hooks; mod modules; mod overlays; diff --git a/tests/modules/mod.rs b/tests/modules/mod.rs index 3bb7a6cee..25a13525c 100644 --- a/tests/modules/mod.rs +++ b/tests/modules/mod.rs @@ -433,3 +433,44 @@ fn module_cyclical_imports_3() { assert!(actual.err.contains("cyclical")); }) } + +#[test] +fn module_import_const_file() { + Playground::setup("module_import_const_file", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "spam.nu", + r#" + export def foo [] { "foo" } + "#, + )]); + + let inp = &[r#"const file = 'spam.nu'"#, r#"use $file foo"#, r#"foo"#]; + + let actual = nu!(cwd: dirs.test(), pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "foo"); + }) +} + +#[test] +fn module_import_const_module_name() { + Playground::setup("module_import_const_file", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "spam.nu", + r#" + export def foo [] { "foo" } + "#, + )]); + + let inp = &[ + r#"module spam { export def foo [] { "foo" } }"#, + r#"const mod = 'spam'"#, + r#"use $mod foo"#, + r#"foo"#, + ]; + + let actual = nu!(cwd: dirs.test(), pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "foo"); + }) +} diff --git a/tests/overlays/mod.rs b/tests/overlays/mod.rs index 277672b6c..bc3a57a6e 100644 --- a/tests/overlays/mod.rs +++ b/tests/overlays/mod.rs @@ -169,6 +169,33 @@ fn add_overlay_from_file_decl() { assert_eq!(actual_repl.out, "foo"); } +#[test] +fn add_overlay_from_const_file_decl() { + let inp = &[ + r#"const file = 'samples/spam.nu'"#, + r#"overlay use $file"#, + r#"foo"#, + ]; + + let actual = nu!(cwd: "tests/overlays", pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "foo"); +} + +#[test] +fn add_overlay_from_const_module_name_decl() { + let inp = &[ + r#"module spam { export def foo [] { "foo" } }"#, + r#"const mod = 'spam'"#, + r#"overlay use $mod"#, + r#"foo"#, + ]; + + let actual = nu!(cwd: "tests/overlays", pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "foo"); +} + // This one tests that the `nu_repl()` loop works correctly #[test] fn add_overlay_from_file_decl_cd() { @@ -693,6 +720,23 @@ fn overlay_add_renamed() { assert_eq!(actual_repl.out, "foo"); } +#[test] +fn overlay_add_renamed_const() { + let inp = &[ + r#"module spam { export def foo [] { "foo" } }"#, + r#"const name = 'spam'"#, + r#"const new_name = 'eggs'"#, + r#"overlay use $name as $new_name --prefix"#, + r#"eggs foo"#, + ]; + + let actual = nu!(cwd: "tests/overlays", pipeline(&inp.join("; "))); + let actual_repl = nu!(cwd: "tests/overlays", nu_repl_code(inp)); + + assert_eq!(actual.out, "foo"); + assert_eq!(actual_repl.out, "foo"); +} + #[test] fn overlay_add_renamed_from_file() { let inp = &[ diff --git a/tests/parsing/mod.rs b/tests/parsing/mod.rs index e84165ca8..d66ab0491 100644 --- a/tests/parsing/mod.rs +++ b/tests/parsing/mod.rs @@ -11,6 +11,17 @@ fn source_file_relative_to_file() { assert_eq!(actual.out, "5"); } +#[test] +fn source_const_file() { + let actual = nu!(cwd: "tests/parsing/samples", + r#" + const file = 'single_line.nu' + source $file + "#); + + assert_eq!(actual.out, "5"); +} + #[test] fn run_nu_script_single_line() { let actual = nu!(cwd: "tests/parsing/samples", r#"