From 5ac5b90aed907aaaa3fa4a10159eaddd5562bb5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BD=C3=A1dn=C3=ADk?= Date: Sat, 26 Aug 2023 16:41:29 +0300 Subject: [PATCH] Allow parse-time evaluation of calls, pipelines and subexpressions (#9499) Co-authored-by: Antoine Stevan <44101798+amtoine@users.noreply.github.com> --- .../nu-cmd-lang/src/core_commands/describe.rs | 83 +++++++++------- crates/nu-cmd-lang/src/core_commands/echo.rs | 55 +++++++---- .../nu-cmd-lang/src/core_commands/ignore.rs | 16 +++- .../nu-cmd-lang/src/core_commands/version.rs | 28 ++++-- crates/nu-command/src/path/basename.rs | 27 +++++- crates/nu-command/src/path/dirname.rs | 28 +++++- crates/nu-command/src/path/exists.rs | 28 +++++- crates/nu-command/src/path/expand.rs | 30 +++++- crates/nu-command/src/path/join.rs | 58 +++++++---- crates/nu-command/src/path/parse.rs | 27 +++++- crates/nu-command/src/path/relative_to.rs | 27 +++++- crates/nu-command/src/path/split.rs | 25 ++++- crates/nu-command/src/path/type.rs | 25 ++++- crates/nu-command/src/strings/str_/length.rs | 35 +++++-- crates/nu-command/tests/commands/echo.rs | 6 ++ .../tests/commands/path/basename.rs | 6 ++ .../nu-command/tests/commands/path/dirname.rs | 6 ++ .../nu-command/tests/commands/path/exists.rs | 6 ++ .../nu-command/tests/commands/path/expand.rs | 20 ++++ crates/nu-command/tests/commands/path/join.rs | 7 ++ crates/nu-command/tests/commands/path/mod.rs | 7 ++ .../nu-command/tests/commands/path/parse.rs | 12 +++ .../nu-command/tests/commands/path/split.rs | 10 ++ .../nu-command/tests/commands/path/type_.rs | 19 ++++ crates/nu-engine/src/call_ext.rs | 67 ++++++++++++- crates/nu-engine/src/env.rs | 51 +++++++++- crates/nu-parser/src/lib.rs | 1 - crates/nu-parser/src/parse_keywords.rs | 30 +++--- crates/nu-parser/src/parser.rs | 6 +- crates/nu-protocol/src/engine/command.rs | 20 +++- crates/nu-protocol/src/engine/engine_state.rs | 6 +- .../eval.rs => nu-protocol/src/eval_const.rs} | 96 +++++++++++++++---- crates/nu-protocol/src/lib.rs | 1 + crates/nu-protocol/src/parse_error.rs | 13 --- crates/nu-protocol/src/shell_error.rs | 68 ++++++++++++- src/tests/test_engine.rs | 11 +-- tests/const_/mod.rs | 49 +++++++++- 37 files changed, 849 insertions(+), 161 deletions(-) rename crates/{nu-parser/src/eval.rs => nu-protocol/src/eval_const.rs} (59%) diff --git a/crates/nu-cmd-lang/src/core_commands/describe.rs b/crates/nu-cmd-lang/src/core_commands/describe.rs index cf8118773..86f0f1496 100644 --- a/crates/nu-cmd-lang/src/core_commands/describe.rs +++ b/crates/nu-cmd-lang/src/core_commands/describe.rs @@ -1,5 +1,5 @@ use nu_protocol::ast::Call; -use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet}; use nu_protocol::{ Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value, }; @@ -27,6 +27,10 @@ impl Command for Describe { .category(Category::Core) } + fn is_const(&self) -> bool { + true + } + fn run( &self, _engine_state: &EngineState, @@ -34,39 +38,16 @@ impl Command for Describe { call: &Call, input: PipelineData, ) -> Result { - let head = call.head; + run(call, input) + } - let no_collect: bool = call.has_flag("no-collect"); - - let description = match input { - PipelineData::ExternalStream { .. } => "raw input".into(), - PipelineData::ListStream(_, _) => { - if no_collect { - "stream".into() - } else { - let value = input.into_value(head); - let base_description = match value { - Value::CustomValue { val, .. } => val.value_string(), - _ => value.get_type().to_string(), - }; - - format!("{base_description} (stream)") - } - } - _ => { - let value = input.into_value(head); - match value { - Value::CustomValue { val, .. } => val.value_string(), - _ => value.get_type().to_string(), - } - } - }; - - Ok(Value::String { - val: description, - span: head, - } - .into_pipeline_data()) + fn run_const( + &self, + _working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, + ) -> Result { + run(call, input) } fn examples(&self) -> Vec { @@ -96,6 +77,42 @@ impl Command for Describe { } } +fn run(call: &Call, input: PipelineData) -> Result { + let head = call.head; + + let no_collect: bool = call.has_flag("no-collect"); + + let description = match input { + PipelineData::ExternalStream { .. } => "raw input".into(), + PipelineData::ListStream(_, _) => { + if no_collect { + "stream".into() + } else { + let value = input.into_value(head); + let base_description = match value { + Value::CustomValue { val, .. } => val.value_string(), + _ => value.get_type().to_string(), + }; + + format!("{base_description} (stream)") + } + } + _ => { + let value = input.into_value(head); + match value { + Value::CustomValue { val, .. } => val.value_string(), + _ => value.get_type().to_string(), + } + } + }; + + Ok(Value::String { + val: description, + span: head, + } + .into_pipeline_data()) +} + #[cfg(test)] mod test { #[test] diff --git a/crates/nu-cmd-lang/src/core_commands/echo.rs b/crates/nu-cmd-lang/src/core_commands/echo.rs index 815bdc776..7056c0921 100644 --- a/crates/nu-cmd-lang/src/core_commands/echo.rs +++ b/crates/nu-cmd-lang/src/core_commands/echo.rs @@ -1,6 +1,6 @@ use nu_engine::CallExt; use nu_protocol::ast::Call; -use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet}; use nu_protocol::{ Category, Example, ListStream, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, @@ -31,6 +31,10 @@ it returns it. Otherwise, it returns a list of the arguments. There is usually little reason to use this over just writing the values as-is."# } + fn is_const(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -38,22 +42,18 @@ little reason to use this over just writing the values as-is."# call: &Call, _input: PipelineData, ) -> Result { - call.rest(engine_state, stack, 0).map(|to_be_echoed| { - let n = to_be_echoed.len(); - match n.cmp(&1usize) { - // More than one value is converted in a stream of values - std::cmp::Ordering::Greater => PipelineData::ListStream( - ListStream::from_stream(to_be_echoed.into_iter(), engine_state.ctrlc.clone()), - None, - ), + let args = call.rest(engine_state, stack, 0); + run(engine_state, args, call) + } - // But a single value can be forwarded as it is - std::cmp::Ordering::Equal => PipelineData::Value(to_be_echoed[0].clone(), None), - - // When there are no elements, we echo the empty string - std::cmp::Ordering::Less => PipelineData::Value(Value::string("", call.head), None), - } - }) + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + _input: PipelineData, + ) -> Result { + let args = call.rest_const(working_set, 0); + run(working_set.permanent(), args, call) } fn examples(&self) -> Vec { @@ -76,6 +76,29 @@ little reason to use this over just writing the values as-is."# } } +fn run( + engine_state: &EngineState, + args: Result, ShellError>, + call: &Call, +) -> Result { + args.map(|to_be_echoed| { + let n = to_be_echoed.len(); + match n.cmp(&1usize) { + // More than one value is converted in a stream of values + std::cmp::Ordering::Greater => PipelineData::ListStream( + ListStream::from_stream(to_be_echoed.into_iter(), engine_state.ctrlc.clone()), + None, + ), + + // But a single value can be forwarded as it is + std::cmp::Ordering::Equal => PipelineData::Value(to_be_echoed[0].clone(), None), + + // When there are no elements, we echo the empty string + std::cmp::Ordering::Less => PipelineData::Value(Value::string("", call.head), None), + } + }) +} + #[cfg(test)] mod test { #[test] diff --git a/crates/nu-cmd-lang/src/core_commands/ignore.rs b/crates/nu-cmd-lang/src/core_commands/ignore.rs index 11680d4e5..f01e52241 100644 --- a/crates/nu-cmd-lang/src/core_commands/ignore.rs +++ b/crates/nu-cmd-lang/src/core_commands/ignore.rs @@ -1,5 +1,5 @@ use nu_protocol::ast::Call; -use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet}; use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Type, Value}; #[derive(Clone)] @@ -24,6 +24,10 @@ impl Command for Ignore { vec!["silent", "quiet", "out-null"] } + fn is_const(&self) -> bool { + true + } + fn run( &self, _engine_state: &EngineState, @@ -35,6 +39,16 @@ impl Command for Ignore { Ok(PipelineData::empty()) } + fn run_const( + &self, + _working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, + ) -> Result { + input.into_value(call.head); + Ok(PipelineData::empty()) + } + fn examples(&self) -> Vec { vec![Example { description: "Ignore the output of an echo command", diff --git a/crates/nu-cmd-lang/src/core_commands/version.rs b/crates/nu-cmd-lang/src/core_commands/version.rs index 19feea0f3..327cd7e2b 100644 --- a/crates/nu-cmd-lang/src/core_commands/version.rs +++ b/crates/nu-cmd-lang/src/core_commands/version.rs @@ -1,5 +1,5 @@ use nu_protocol::ast::Call; -use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet}; use nu_protocol::{ Category, Example, IntoPipelineData, PipelineData, Record, ShellError, Signature, Type, Value, }; @@ -26,14 +26,27 @@ impl Command for Version { "Display Nu version, and its build configuration." } + fn is_const(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, - stack: &mut Stack, + _stack: &mut Stack, call: &Call, - input: PipelineData, + _input: PipelineData, ) -> Result { - version(engine_state, stack, call, input) + version(engine_state, call) + } + + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + _input: PipelineData, + ) -> Result { + version(working_set.permanent(), call) } fn examples(&self) -> Vec { @@ -45,12 +58,7 @@ impl Command for Version { } } -pub fn version( - engine_state: &EngineState, - _stack: &mut Stack, - call: &Call, - _input: PipelineData, -) -> Result { +pub fn version(engine_state: &EngineState, call: &Call) -> Result { // Pre-allocate the arrays in the worst case (12 items): // - version // - branch diff --git a/crates/nu-command/src/path/basename.rs b/crates/nu-command/src/path/basename.rs index 562a3464c..b069c6b17 100644 --- a/crates/nu-command/src/path/basename.rs +++ b/crates/nu-command/src/path/basename.rs @@ -3,7 +3,7 @@ use std::path::Path; use super::PathSubcommandArguments; use nu_engine::CallExt; use nu_protocol::ast::Call; -use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; use nu_protocol::{ engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, @@ -45,6 +45,10 @@ impl Command for SubCommand { "Get the final component of a path." } + fn is_const(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -67,6 +71,27 @@ impl Command for SubCommand { ) } + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments { + replace: call.get_flag_const(working_set, "replace")?, + }; + + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map( + move |value| super::operate(&get_basename, &args, value, head), + working_set.permanent().ctrlc.clone(), + ) + } + #[cfg(windows)] fn examples(&self) -> Vec { vec![ diff --git a/crates/nu-command/src/path/dirname.rs b/crates/nu-command/src/path/dirname.rs index 95b2a8e82..fbbf91bf8 100644 --- a/crates/nu-command/src/path/dirname.rs +++ b/crates/nu-command/src/path/dirname.rs @@ -2,7 +2,7 @@ use std::path::Path; use nu_engine::CallExt; use nu_protocol::ast::Call; -use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; use nu_protocol::{ engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, @@ -53,6 +53,10 @@ impl Command for SubCommand { "Get the parent directory of a path." } + fn is_const(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -76,6 +80,28 @@ impl Command for SubCommand { ) } + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments { + replace: call.get_flag_const(working_set, "replace")?, + num_levels: call.get_flag_const(working_set, "num-levels")?, + }; + + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map( + move |value| super::operate(&get_dirname, &args, value, head), + working_set.permanent().ctrlc.clone(), + ) + } + #[cfg(windows)] fn examples(&self) -> Vec { vec![ diff --git a/crates/nu-command/src/path/exists.rs b/crates/nu-command/src/path/exists.rs index a531a092a..694ce4efc 100644 --- a/crates/nu-command/src/path/exists.rs +++ b/crates/nu-command/src/path/exists.rs @@ -1,9 +1,9 @@ use std::path::{Path, PathBuf}; -use nu_engine::current_dir; +use nu_engine::{current_dir, current_dir_const}; use nu_path::expand_path_with; use nu_protocol::ast::Call; -use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; use nu_protocol::{ engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Type, Value, }; @@ -45,6 +45,10 @@ impl Command for SubCommand { If you need to distinguish dirs and files, please use `path type`."# } + fn is_const(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -66,6 +70,26 @@ If you need to distinguish dirs and files, please use `path type`."# ) } + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments { + pwd: current_dir_const(working_set)?, + }; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map( + move |value| super::operate(&exists, &args, value, head), + working_set.permanent().ctrlc.clone(), + ) + } + #[cfg(windows)] fn examples(&self) -> Vec { vec![ diff --git a/crates/nu-command/src/path/expand.rs b/crates/nu-command/src/path/expand.rs index 3aacd9f10..bf58b6990 100644 --- a/crates/nu-command/src/path/expand.rs +++ b/crates/nu-command/src/path/expand.rs @@ -1,9 +1,9 @@ use std::path::Path; -use nu_engine::env::current_dir_str; +use nu_engine::env::{current_dir_str, current_dir_str_const}; use nu_path::{canonicalize_with, expand_path_with}; use nu_protocol::ast::Call; -use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; use nu_protocol::{ engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Type, Value, }; @@ -48,6 +48,10 @@ impl Command for SubCommand { "Try to expand a path to its absolute form." } + fn is_const(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -71,6 +75,28 @@ impl Command for SubCommand { ) } + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments { + strict: call.has_flag("strict"), + cwd: current_dir_str_const(working_set)?, + not_follow_symlink: call.has_flag("no-symlink"), + }; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map( + move |value| super::operate(&expand, &args, value, head), + working_set.permanent().ctrlc.clone(), + ) + } + #[cfg(windows)] fn examples(&self) -> Vec { vec![ diff --git a/crates/nu-command/src/path/join.rs b/crates/nu-command/src/path/join.rs index 4acde4cfa..13e9285e8 100644 --- a/crates/nu-command/src/path/join.rs +++ b/crates/nu-command/src/path/join.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use nu_engine::CallExt; use nu_protocol::ast::Call; -use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; use nu_protocol::{ engine::Command, Category, Example, PipelineData, Record, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, @@ -46,6 +46,10 @@ impl Command for SubCommand { the output of 'path parse' and 'path split' subcommands."# } + fn is_const(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -53,29 +57,24 @@ the output of 'path parse' and 'path split' subcommands."# call: &Call, input: PipelineData, ) -> Result { - let head = call.head; let args = Arguments { append: call.rest(engine_state, stack, 0)?, }; - let metadata = input.metadata(); + run(call, &args, input) + } - match input { - PipelineData::Value(val, md) => { - Ok(PipelineData::Value(handle_value(val, &args, head), md)) - } - PipelineData::ListStream(..) => Ok(PipelineData::Value( - handle_value(input.into_value(head), &args, head), - metadata, - )), - PipelineData::Empty { .. } => Err(ShellError::PipelineEmpty { dst_span: head }), - _ => Err(ShellError::UnsupportedInput( - "Input value cannot be joined".to_string(), - "value originates from here".into(), - head, - input.span().unwrap_or(call.head), - )), - } + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, + ) -> Result { + let args = Arguments { + append: call.rest_const(working_set, 0)?, + }; + + run(call, &args, input) } #[cfg(windows)] @@ -147,6 +146,27 @@ the output of 'path parse' and 'path split' subcommands."# } } +fn run(call: &Call, args: &Arguments, input: PipelineData) -> Result { + let head = call.head; + + let metadata = input.metadata(); + + match input { + PipelineData::Value(val, md) => Ok(PipelineData::Value(handle_value(val, args, head), md)), + PipelineData::ListStream(..) => Ok(PipelineData::Value( + handle_value(input.into_value(head), args, head), + metadata, + )), + PipelineData::Empty { .. } => Err(ShellError::PipelineEmpty { dst_span: head }), + _ => Err(ShellError::UnsupportedInput( + "Input value cannot be joined".to_string(), + "value originates from here".into(), + head, + input.span().unwrap_or(call.head), + )), + } +} + fn handle_value(v: Value, args: &Arguments, head: Span) -> Value { match v { Value::String { ref val, .. } => join_single(Path::new(val), head, args), diff --git a/crates/nu-command/src/path/parse.rs b/crates/nu-command/src/path/parse.rs index 483e97367..a4cd8f8fb 100644 --- a/crates/nu-command/src/path/parse.rs +++ b/crates/nu-command/src/path/parse.rs @@ -2,7 +2,7 @@ use std::path::Path; use nu_engine::CallExt; use nu_protocol::ast::Call; -use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; use nu_protocol::{ engine::Command, Category, Example, PipelineData, Record, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, @@ -48,6 +48,10 @@ impl Command for SubCommand { On Windows, an extra 'prefix' column is added."# } + fn is_const(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -70,6 +74,27 @@ On Windows, an extra 'prefix' column is added."# ) } + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments { + extension: call.get_flag_const(working_set, "extension")?, + }; + + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map( + move |value| super::operate(&parse, &args, value, head), + working_set.permanent().ctrlc.clone(), + ) + } + #[cfg(windows)] fn examples(&self) -> Vec { vec![ diff --git a/crates/nu-command/src/path/relative_to.rs b/crates/nu-command/src/path/relative_to.rs index 15f80ff1b..ac6bffaf8 100644 --- a/crates/nu-command/src/path/relative_to.rs +++ b/crates/nu-command/src/path/relative_to.rs @@ -3,7 +3,7 @@ use std::path::Path; use nu_engine::CallExt; use nu_path::expand_to_real_path; use nu_protocol::ast::Call; -use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; use nu_protocol::{ engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, @@ -52,6 +52,10 @@ absolute or both relative. The argument path needs to be a parent of the input path."# } + fn is_const(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -74,6 +78,27 @@ path."# ) } + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments { + path: call.req_const(working_set, 0)?, + }; + + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map( + move |value| super::operate(&relative_to, &args, value, head), + working_set.permanent().ctrlc.clone(), + ) + } + #[cfg(windows)] fn examples(&self) -> Vec { vec![ diff --git a/crates/nu-command/src/path/split.rs b/crates/nu-command/src/path/split.rs index ab70d4966..349b80b02 100644 --- a/crates/nu-command/src/path/split.rs +++ b/crates/nu-command/src/path/split.rs @@ -1,7 +1,7 @@ use std::path::{Component, Path}; use nu_protocol::ast::Call; -use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; use nu_protocol::{ engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Type, Value, }; @@ -36,6 +36,10 @@ impl Command for SubCommand { "Split a path into a list based on the system's path separator." } + fn is_const(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -56,6 +60,25 @@ impl Command for SubCommand { ) } + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments; + + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map( + move |value| super::operate(&split, &args, value, head), + working_set.permanent().ctrlc.clone(), + ) + } + #[cfg(windows)] fn examples(&self) -> Vec { vec![ diff --git a/crates/nu-command/src/path/type.rs b/crates/nu-command/src/path/type.rs index 2660cbf2f..39d6ebb06 100644 --- a/crates/nu-command/src/path/type.rs +++ b/crates/nu-command/src/path/type.rs @@ -2,7 +2,7 @@ use std::path::Path; use nu_path::expand_tilde; use nu_protocol::ast::Call; -use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; use nu_protocol::{ engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Type, Value, }; @@ -43,6 +43,10 @@ impl Command for SubCommand { If nothing is found, an empty string will be returned."# } + fn is_const(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -63,6 +67,25 @@ If nothing is found, an empty string will be returned."# ) } + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments; + + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map( + move |value| super::operate(&r#type, &args, value, head), + working_set.permanent().ctrlc.clone(), + ) + } + fn examples(&self) -> Vec { vec![ Example { diff --git a/crates/nu-command/src/strings/str_/length.rs b/crates/nu-command/src/strings/str_/length.rs index ef1b07f8c..4c3571687 100644 --- a/crates/nu-command/src/strings/str_/length.rs +++ b/crates/nu-command/src/strings/str_/length.rs @@ -3,7 +3,7 @@ use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::ast::CellPath; -use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet}; use nu_protocol::Category; use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value}; use unicode_segmentation::UnicodeSegmentation; @@ -62,6 +62,10 @@ impl Command for SubCommand { vec!["size", "count"] } + fn is_const(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -70,11 +74,17 @@ impl Command for SubCommand { input: PipelineData, ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; - let args = Arguments { - cell_paths: (!cell_paths.is_empty()).then_some(cell_paths), - graphemes: grapheme_flags(call)?, - }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + run(cell_paths, engine_state, call, input) + } + + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, + ) -> Result { + let cell_paths: Vec = call.rest_const(working_set, 0)?; + run(cell_paths, working_set.permanent(), call, input) } fn examples(&self) -> Vec { @@ -101,6 +111,19 @@ impl Command for SubCommand { } } +fn run( + cell_paths: Vec, + engine_state: &EngineState, + call: &Call, + input: PipelineData, +) -> Result { + let args = Arguments { + cell_paths: (!cell_paths.is_empty()).then_some(cell_paths), + graphemes: grapheme_flags(call)?, + }; + operate(action, args, input, call.head, engine_state.ctrlc.clone()) +} + fn action(input: &Value, arg: &Arguments, head: Span) -> Value { match input { Value::String { val, .. } => Value::int( diff --git a/crates/nu-command/tests/commands/echo.rs b/crates/nu-command/tests/commands/echo.rs index 44e6d8145..1147f45df 100644 --- a/crates/nu-command/tests/commands/echo.rs +++ b/crates/nu-command/tests/commands/echo.rs @@ -34,3 +34,9 @@ fn echo_range_handles_exclusive_down() { assert_eq!(actual.out, "[3,2]"); } + +#[test] +fn echo_const() { + let actual = nu!("const x = (echo spam); $x"); + assert_eq!(actual.out, "spam"); +} diff --git a/crates/nu-command/tests/commands/path/basename.rs b/crates/nu-command/tests/commands/path/basename.rs index 436ea0c6c..65ca7abdc 100644 --- a/crates/nu-command/tests/commands/path/basename.rs +++ b/crates/nu-command/tests/commands/path/basename.rs @@ -81,3 +81,9 @@ fn replaces_basename_of_path_ending_with_double_dot() { let expected = join_path_sep(&["some/file.txt/..", "eggs"]); assert_eq!(actual.out, expected); } + +#[test] +fn const_path_basename() { + let actual = nu!("const name = ('spam/eggs.txt' | path basename); $name"); + assert_eq!(actual.out, "eggs.txt"); +} diff --git a/crates/nu-command/tests/commands/path/dirname.rs b/crates/nu-command/tests/commands/path/dirname.rs index d9a50acbc..5741238b7 100644 --- a/crates/nu-command/tests/commands/path/dirname.rs +++ b/crates/nu-command/tests/commands/path/dirname.rs @@ -135,3 +135,9 @@ fn replaces_dirname_of_way_too_many_levels() { let expected = join_path_sep(&["eggs", "some/dir/with/spam.txt"]); assert_eq!(actual.out, expected); } + +#[test] +fn const_path_dirname() { + let actual = nu!("const name = ('spam/eggs.txt' | path dirname); $name"); + assert_eq!(actual.out, "spam"); +} diff --git a/crates/nu-command/tests/commands/path/exists.rs b/crates/nu-command/tests/commands/path/exists.rs index 9a050ed0f..5db22033e 100644 --- a/crates/nu-command/tests/commands/path/exists.rs +++ b/crates/nu-command/tests/commands/path/exists.rs @@ -57,3 +57,9 @@ fn checks_tilde_relative_path_exists() { let actual = nu!("'~' | path exists"); assert_eq!(actual.out, "true"); } + +#[test] +fn const_path_exists() { + let actual = nu!("const exists = ('~' | path exists); $exists"); + assert_eq!(actual.out, "true"); +} diff --git a/crates/nu-command/tests/commands/path/expand.rs b/crates/nu-command/tests/commands/path/expand.rs index d9168ac6c..f3bdeb817 100644 --- a/crates/nu-command/tests/commands/path/expand.rs +++ b/crates/nu-command/tests/commands/path/expand.rs @@ -66,6 +66,26 @@ fn expands_path_with_double_dot() { }) } +#[test] +fn const_path_expand() { + Playground::setup("const_path_expand", |dirs, sandbox| { + sandbox + .within("menu") + .with_files(vec![EmptyFile("spam.txt")]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + const result = ("menu/./spam.txt" | path expand); + $result + "# + )); + + let expected = dirs.test.join("menu").join("spam.txt"); + assert_eq!(PathBuf::from(actual.out), expected); + }) +} + #[cfg(windows)] mod windows { use super::*; diff --git a/crates/nu-command/tests/commands/path/join.rs b/crates/nu-command/tests/commands/path/join.rs index b7f5f0781..f1cd48616 100644 --- a/crates/nu-command/tests/commands/path/join.rs +++ b/crates/nu-command/tests/commands/path/join.rs @@ -54,3 +54,10 @@ fn returns_joined_path_when_joining_empty_path() { assert_eq!(actual.out, "foo.txt"); } + +#[test] +fn const_path_join() { + let actual = nu!("const name = ('spam' | path join 'eggs.txt'); $name"); + let expected = join_path_sep(&["spam", "eggs.txt"]); + assert_eq!(actual.out, expected); +} diff --git a/crates/nu-command/tests/commands/path/mod.rs b/crates/nu-command/tests/commands/path/mod.rs index c836c5691..84bb2d484 100644 --- a/crates/nu-command/tests/commands/path/mod.rs +++ b/crates/nu-command/tests/commands/path/mod.rs @@ -7,6 +7,7 @@ mod parse; mod split; mod type_; +use nu_test_support::{nu, pipeline}; use std::path::MAIN_SEPARATOR; /// Helper function that joins string literals with '/' or '\', based on host OS @@ -32,3 +33,9 @@ fn joins_path_on_other_than_windows() { assert_eq!(&actual, "sausage/bacon/spam"); } + +#[test] +fn const_path_relative_to() { + let actual = nu!("'/home/viking' | path relative-to '/home'"); + assert_eq!(actual.out, "viking"); +} diff --git a/crates/nu-command/tests/commands/path/parse.rs b/crates/nu-command/tests/commands/path/parse.rs index b1607556c..009cdbfce 100644 --- a/crates/nu-command/tests/commands/path/parse.rs +++ b/crates/nu-command/tests/commands/path/parse.rs @@ -119,3 +119,15 @@ fn parses_into_correct_number_of_columns() { assert_eq!(actual.out, expected); } + +#[test] +fn const_path_parse() { + let actual = nu!("const name = ('spam/eggs.txt' | path parse); $name.parent"); + assert_eq!(actual.out, "spam"); + + let actual = nu!("const name = ('spam/eggs.txt' | path parse); $name.stem"); + assert_eq!(actual.out, "eggs"); + + let actual = nu!("const name = ('spam/eggs.txt' | path parse); $name.extension"); + assert_eq!(actual.out, "txt"); +} diff --git a/crates/nu-command/tests/commands/path/split.rs b/crates/nu-command/tests/commands/path/split.rs index b6a0c2295..4728f0a64 100644 --- a/crates/nu-command/tests/commands/path/split.rs +++ b/crates/nu-command/tests/commands/path/split.rs @@ -25,3 +25,13 @@ fn splits_correctly_single_path() { assert_eq!(actual.out, "spam.txt"); } + +#[test] +fn splits_correctly_single_path_const() { + let actual = nu!(r#" + const result = ('home/viking/spam.txt' | path split); + $result | last + "#); + + assert_eq!(actual.out, "spam.txt"); +} diff --git a/crates/nu-command/tests/commands/path/type_.rs b/crates/nu-command/tests/commands/path/type_.rs index 71a86b384..f2ba95fd3 100644 --- a/crates/nu-command/tests/commands/path/type_.rs +++ b/crates/nu-command/tests/commands/path/type_.rs @@ -61,3 +61,22 @@ fn returns_type_of_existing_directory() { assert_eq!(actual.out, "dir"); }) } + +#[test] +fn returns_type_of_existing_file_const() { + Playground::setup("path_type_const", |dirs, sandbox| { + sandbox + .within("menu") + .with_files(vec![EmptyFile("spam.txt")]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + const ty = ("menu" | path type); + $ty + "# + )); + + assert_eq!(actual.out, "dir"); + }) +} diff --git a/crates/nu-engine/src/call_ext.rs b/crates/nu-engine/src/call_ext.rs index d3c543aab..6bad65326 100644 --- a/crates/nu-engine/src/call_ext.rs +++ b/crates/nu-engine/src/call_ext.rs @@ -1,6 +1,7 @@ use nu_protocol::{ ast::Call, - engine::{EngineState, Stack}, + engine::{EngineState, Stack, StateWorkingSet}, + eval_const::eval_constant, FromValue, ShellError, }; @@ -14,6 +15,12 @@ pub trait CallExt { name: &str, ) -> Result, ShellError>; + fn get_flag_const( + &self, + working_set: &StateWorkingSet, + name: &str, + ) -> Result, ShellError>; + fn rest( &self, engine_state: &EngineState, @@ -21,6 +28,12 @@ pub trait CallExt { starting_pos: usize, ) -> Result, ShellError>; + fn rest_const( + &self, + working_set: &StateWorkingSet, + starting_pos: usize, + ) -> Result, ShellError>; + fn opt( &self, engine_state: &EngineState, @@ -35,6 +48,12 @@ pub trait CallExt { pos: usize, ) -> Result; + fn req_const( + &self, + working_set: &StateWorkingSet, + pos: usize, + ) -> Result; + fn req_parser_info( &self, engine_state: &EngineState, @@ -58,6 +77,19 @@ impl CallExt for Call { } } + fn get_flag_const( + &self, + working_set: &StateWorkingSet, + name: &str, + ) -> Result, ShellError> { + if let Some(expr) = self.get_flag_expr(name) { + let result = eval_constant(working_set, &expr)?; + FromValue::from_value(&result).map(Some) + } else { + Ok(None) + } + } + fn rest( &self, engine_state: &EngineState, @@ -74,6 +106,21 @@ impl CallExt for Call { Ok(output) } + fn rest_const( + &self, + working_set: &StateWorkingSet, + starting_pos: usize, + ) -> Result, ShellError> { + let mut output = vec![]; + + for expr in self.positional_iter().skip(starting_pos) { + let result = eval_constant(working_set, expr)?; + output.push(FromValue::from_value(&result)?); + } + + Ok(output) + } + fn opt( &self, engine_state: &EngineState, @@ -107,6 +154,24 @@ impl CallExt for Call { } } + fn req_const( + &self, + working_set: &StateWorkingSet, + pos: usize, + ) -> Result { + if let Some(expr) = self.positional_nth(pos) { + let result = eval_constant(working_set, expr)?; + FromValue::from_value(&result) + } else if self.positional_len() == 0 { + Err(ShellError::AccessEmptyContent { span: self.head }) + } else { + Err(ShellError::AccessBeyondEnd { + max_idx: self.positional_len() - 1, + span: self.head, + }) + } + } + fn req_parser_info( &self, engine_state: &EngineState, diff --git a/crates/nu-engine/src/env.rs b/crates/nu-engine/src/env.rs index bc2265d6e..d28d8fe71 100644 --- a/crates/nu-engine/src/env.rs +++ b/crates/nu-engine/src/env.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use nu_protocol::ast::{Call, Expr, PathMember}; -use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::engine::{EngineState, Stack, StateWorkingSet, PWD_ENV}; use nu_protocol::{Config, PipelineData, ShellError, Span, Value, VarId}; use nu_path::canonicalize_with; @@ -159,8 +159,9 @@ pub fn env_to_strings( /// Shorthand for env_to_string() for PWD with custom error pub fn current_dir_str(engine_state: &EngineState, stack: &Stack) -> Result { - if let Some(pwd) = stack.get_env_var(engine_state, "PWD") { - match env_to_string("PWD", &pwd, engine_state, stack) { + if let Some(pwd) = stack.get_env_var(engine_state, PWD_ENV) { + // TODO: PWD should be string by default, we don't need to run ENV_CONVERSIONS on it + match env_to_string(PWD_ENV, &pwd, engine_state, stack) { Ok(cwd) => { if Path::new(&cwd).is_absolute() { Ok(cwd) @@ -187,11 +188,55 @@ pub fn current_dir_str(engine_state: &EngineState, stack: &Stack) -> Result Result { + if let Some(pwd) = working_set.get_env_var(PWD_ENV) { + match pwd { + Value::String { val, span } => { + if Path::new(val).is_absolute() { + Ok(val.clone()) + } else { + Err(ShellError::GenericError( + "Invalid current directory".to_string(), + format!("The 'PWD' environment variable must be set to an absolute path. Found: '{val}'"), + Some(*span), + None, + Vec::new() + )) + } + } + _ => Err(ShellError::GenericError( + "PWD is not a string".to_string(), + "".to_string(), + None, + Some( + "Cusrrent working directory environment variable 'PWD' must be a string." + .to_string(), + ), + Vec::new(), + )), + } + } else { + Err(ShellError::GenericError( + "Current directory not found".to_string(), + "".to_string(), + None, + Some("The environment variable 'PWD' was not found. It is required to define the current directory.".to_string()), + Vec::new(), + )) + } +} + /// Calls current_dir_str() and returns the current directory as a PathBuf pub fn current_dir(engine_state: &EngineState, stack: &Stack) -> Result { current_dir_str(engine_state, stack).map(PathBuf::from) } +/// Version of current_dir() for constant evaluation +pub fn current_dir_const(working_set: &StateWorkingSet) -> Result { + current_dir_str_const(working_set).map(PathBuf::from) +} + /// Get the contents of path environment variable as a list of strings /// /// On non-Windows: It will fetch PATH diff --git a/crates/nu-parser/src/lib.rs b/crates/nu-parser/src/lib.rs index c8842ad0e..d8ff5b1f8 100644 --- a/crates/nu-parser/src/lib.rs +++ b/crates/nu-parser/src/lib.rs @@ -1,5 +1,4 @@ mod deparse; -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 acdcffc73..892312cd8 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -12,6 +12,7 @@ use nu_protocol::{ ImportPatternMember, Pipeline, PipelineElement, }, engine::{StateWorkingSet, DEFAULT_OVERLAY_NAME}, + eval_const::{eval_constant, value_as_string}, span, Alias, BlockId, Exportable, Module, ModuleId, ParseError, PositionalArg, ResolvedImportPattern, Span, Spanned, SyntaxShape, Type, VarId, }; @@ -24,7 +25,6 @@ pub const LIB_DIRS_VAR: &str = "NU_LIB_DIRS"; pub const PLUGIN_DIRS_VAR: &str = "NU_PLUGIN_DIRS"; use crate::{ - eval::{eval_constant, value_as_string}, is_math_expression_like, known_external::KnownExternal, lex, @@ -2585,12 +2585,12 @@ pub fn parse_overlay_new(working_set: &mut StateWorkingSet, call: Box) -> Ok(val) => match value_as_string(val, expr.span) { Ok(s) => (s, expr.span), Err(err) => { - working_set.error(err); + working_set.error(err.wrap(working_set, call_span)); return garbage_pipeline(&[call_span]); } }, Err(err) => { - working_set.error(err); + working_set.error(err.wrap(working_set, call_span)); return garbage_pipeline(&[call_span]); } } @@ -2634,12 +2634,12 @@ pub fn parse_overlay_use(working_set: &mut StateWorkingSet, call: Box) -> Ok(val) => match value_as_string(val, expr.span) { Ok(s) => (s, expr.span), Err(err) => { - working_set.error(err); + working_set.error(err.wrap(working_set, call_span)); return garbage_pipeline(&[call_span]); } }, Err(err) => { - working_set.error(err); + working_set.error(err.wrap(working_set, call_span)); return garbage_pipeline(&[call_span]); } } @@ -2660,12 +2660,12 @@ pub fn parse_overlay_use(working_set: &mut StateWorkingSet, call: Box) -> span: new_name_expression.span, }), Err(err) => { - working_set.error(err); + working_set.error(err.wrap(working_set, call_span)); return garbage_pipeline(&[call_span]); } }, Err(err) => { - working_set.error(err); + working_set.error(err.wrap(working_set, call_span)); return garbage_pipeline(&[call_span]); } } @@ -2851,12 +2851,12 @@ pub fn parse_overlay_hide(working_set: &mut StateWorkingSet, call: Box) -> Ok(val) => match value_as_string(val, expr.span) { Ok(s) => (s, expr.span), Err(err) => { - working_set.error(err); + working_set.error(err.wrap(working_set, call_span)); return garbage_pipeline(&[call_span]); } }, Err(err) => { - working_set.error(err); + working_set.error(err.wrap(working_set, call_span)); return garbage_pipeline(&[call_span]); } } @@ -3107,7 +3107,7 @@ pub fn parse_const(working_set: &mut StateWorkingSet, spans: &[Span]) -> Pipelin // Assign the constant value to the variable working_set.set_variable_const_val(var_id, val); } - Err(err) => working_set.error(err), + Err(err) => working_set.error(err.wrap(working_set, rvalue.span)), } } @@ -3300,7 +3300,7 @@ pub fn parse_source(working_set: &mut StateWorkingSet, spans: &[Span]) -> Pipeli let val = match eval_constant(working_set, &expr) { Ok(val) => val, Err(err) => { - working_set.error(err); + working_set.error(err.wrap(working_set, span(&spans[1..]))); return Pipeline::from_vec(vec![Expression { expr: Expr::Call(call), span: span(&spans[1..]), @@ -3313,7 +3313,7 @@ pub fn parse_source(working_set: &mut StateWorkingSet, spans: &[Span]) -> Pipeli let filename = match value_as_string(val, spans[1]) { Ok(s) => s, Err(err) => { - working_set.error(err); + working_set.error(err.wrap(working_set, span(&spans[1..]))); return Pipeline::from_vec(vec![Expression { expr: Expr::Call(call), span: span(&spans[1..]), @@ -3504,8 +3504,10 @@ pub fn parse_register(working_set: &mut StateWorkingSet, spans: &[Span]) -> Pipe let arguments = call .positional_nth(0) .map(|expr| { - let val = eval_constant(working_set, expr)?; - let filename = value_as_string(val, expr.span)?; + let val = + eval_constant(working_set, expr).map_err(|err| err.wrap(working_set, call.head))?; + let filename = + value_as_string(val, expr.span).map_err(|err| err.wrap(working_set, call.head))?; let Some(path) = find_in_dirs(&filename, working_set, &cwd, PLUGIN_DIRS_VAR) else { return Err(ParseError::RegisteredFileNotFound(filename, expr.span)) diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 0b33085d4..85a510f9b 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -1,5 +1,4 @@ use crate::{ - eval::{eval_constant, value_as_string}, lex::{lex, lex_signature}, lite_parser::{lite_parse, LiteCommand, LiteElement, LitePipeline}, parse_mut, @@ -16,6 +15,7 @@ use nu_protocol::{ Operator, PathMember, Pattern, Pipeline, PipelineElement, RangeInclusion, RangeOperator, }, engine::StateWorkingSet, + eval_const::{eval_constant, value_as_string}, span, BlockId, DidYouMean, Flag, ParseError, PositionalArg, Signature, Span, Spanned, SyntaxShape, Type, Unit, VarId, ENV_VARIABLE_ID, IN_VARIABLE_ID, }; @@ -2959,12 +2959,12 @@ pub fn parse_import_pattern(working_set: &mut StateWorkingSet, spans: &[Span]) - Ok(val) => match value_as_string(val, head_expr.span) { Ok(s) => (working_set.find_module(s.as_bytes()), s.into_bytes()), Err(err) => { - working_set.error(err); + working_set.error(err.wrap(working_set, span(spans))); return garbage(span(spans)); } }, Err(err) => { - working_set.error(err); + working_set.error(err.wrap(working_set, span(spans))); return garbage(span(spans)); } }; diff --git a/crates/nu-protocol/src/engine/command.rs b/crates/nu-protocol/src/engine/command.rs index 084bbf42b..8c27e51a6 100644 --- a/crates/nu-protocol/src/engine/command.rs +++ b/crates/nu-protocol/src/engine/command.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use crate::{ast::Call, Alias, BlockId, Example, PipelineData, ShellError, Signature}; -use super::{EngineState, Stack}; +use super::{EngineState, Stack, StateWorkingSet}; #[derive(Debug)] pub enum CommandType { @@ -34,6 +34,19 @@ pub trait Command: Send + Sync + CommandClone { input: PipelineData, ) -> Result; + /// Used by the parser to run command at parse time + /// + /// If a command has `is_const()` set to true, it must also implement this method. + #[allow(unused_variables)] + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, + ) -> Result { + Err(ShellError::MissingConstEvalImpl { span: call.head }) + } + fn examples(&self) -> Vec { Vec::new() } @@ -83,6 +96,11 @@ pub trait Command: Send + Sync + CommandClone { None } + // Whether can run in const evaluation in the parser + fn is_const(&self) -> bool { + false + } + // If command is a block i.e. def blah [] { }, get the block id fn get_block_id(&self) -> Option { None diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 6262ea4d5..385bd32d0 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -19,7 +19,7 @@ use std::sync::{ Arc, Mutex, }; -static PWD_ENV: &str = "PWD"; +pub static PWD_ENV: &str = "PWD"; /// Organizes usage messages for various primitives #[derive(Debug, Clone)] @@ -1090,6 +1090,10 @@ impl<'a> StateWorkingSet<'a> { } } + pub fn permanent(&self) -> &EngineState { + self.permanent_state + } + pub fn error(&mut self, parse_error: ParseError) { self.parse_errors.push(parse_error) } diff --git a/crates/nu-parser/src/eval.rs b/crates/nu-protocol/src/eval_const.rs similarity index 59% rename from crates/nu-parser/src/eval.rs rename to crates/nu-protocol/src/eval_const.rs index bed158d41..5d5351f0a 100644 --- a/crates/nu-parser/src/eval.rs +++ b/crates/nu-protocol/src/eval_const.rs @@ -1,16 +1,70 @@ -use nu_protocol::{ - ast::{Expr, Expression}, +use crate::{ + ast::{Block, Call, Expr, Expression, PipelineElement}, engine::StateWorkingSet, - ParseError, Record, Span, Value, + PipelineData, Record, ShellError, Span, Value, }; +fn eval_const_call( + working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, +) -> Result { + let decl = working_set.get_decl(call.decl_id); + + if !decl.is_const() { + return Err(ShellError::NotAConstCommand(call.head)); + } + + if !decl.is_known_external() && call.named_iter().any(|(flag, _, _)| flag.item == "help") { + // It would require re-implementing get_full_help() for const evaluation. Assuming that + // getting help messages at parse-time is rare enough, we can simply disallow it. + return Err(ShellError::NotAConstHelp(call.head)); + } + + decl.run_const(working_set, call, input) +} + +fn eval_const_subexpression( + working_set: &StateWorkingSet, + expr: &Expression, + block: &Block, + mut input: PipelineData, +) -> Result { + for pipeline in block.pipelines.iter() { + for element in pipeline.elements.iter() { + let PipelineElement::Expression(_, expr) = element else { + return Err(ShellError::NotAConstant(expr.span)); + }; + + input = eval_constant_with_input(working_set, expr, input)? + } + } + + Ok(input) +} + +fn eval_constant_with_input( + working_set: &StateWorkingSet, + expr: &Expression, + input: PipelineData, +) -> Result { + match &expr.expr { + Expr::Call(call) => eval_const_call(working_set, call, input), + Expr::Subexpression(block_id) => { + let block = working_set.get_block(*block_id); + eval_const_subexpression(working_set, expr, block, input) + } + _ => eval_constant(working_set, expr).map(|v| PipelineData::Value(v, None)), + } +} + /// Evaluate a constant value at parse time /// /// Based off eval_expression() in the engine pub fn eval_constant( working_set: &StateWorkingSet, expr: &Expression, -) -> Result { +) -> Result { match &expr.expr { Expr::Bool(b) => Ok(Value::bool(*b, expr.span)), Expr::Int(i) => Ok(Value::int(*i, expr.span)), @@ -25,7 +79,7 @@ pub fn eval_constant( }), Expr::Var(var_id) => match working_set.get_variable(*var_id).const_val.as_ref() { Some(val) => Ok(val.clone()), - None => Err(ParseError::NotAConstant(expr.span)), + None => Err(ShellError::NotAConstant(expr.span)), }, Expr::CellPath(cell_path) => Ok(Value::CellPath { val: cell_path.clone(), @@ -37,10 +91,12 @@ pub fn eval_constant( match value.follow_cell_path(&cell_path.tail, false) { Ok(val) => Ok(val), // TODO: Better error conversion - Err(shell_error) => Err(ParseError::LabeledError( + Err(shell_error) => Err(ShellError::GenericError( "Error when following cell path".to_string(), format!("{shell_error:?}"), - expr.span, + Some(expr.span), + None, + vec![], )), } } @@ -112,25 +168,29 @@ pub fn eval_constant( Expr::Nothing => Ok(Value::Nothing { span: expr.span }), Expr::ValueWithUnit(expr, unit) => { if let Ok(Value::Int { val, .. }) = eval_constant(working_set, expr) { - unit.item.to_value(val, unit.span).map_err(|_| { - ParseError::InvalidLiteral( - "literal can not fit in unit".into(), - "literal can not fit in unit".into(), - unit.span, - ) - }) + unit.item.to_value(val, unit.span) } else { - Err(ParseError::NotAConstant(expr.span)) + Err(ShellError::NotAConstant(expr.span)) } } - _ => Err(ParseError::NotAConstant(expr.span)), + Expr::Call(call) => { + Ok(eval_const_call(working_set, call, PipelineData::empty())?.into_value(expr.span)) + } + Expr::Subexpression(block_id) => { + let block = working_set.get_block(*block_id); + Ok( + eval_const_subexpression(working_set, expr, block, PipelineData::empty())? + .into_value(expr.span), + ) + } + _ => Err(ShellError::NotAConstant(expr.span)), } } /// Get the value as a string -pub fn value_as_string(value: Value, span: Span) -> Result { +pub fn value_as_string(value: Value, span: Span) -> Result { match value { Value::String { val, .. } => Ok(val), - _ => Err(ParseError::NotAConstant(span)), + _ => Err(ShellError::NotAConstant(span)), } } diff --git a/crates/nu-protocol/src/lib.rs b/crates/nu-protocol/src/lib.rs index ad695a1b9..854f5aa30 100644 --- a/crates/nu-protocol/src/lib.rs +++ b/crates/nu-protocol/src/lib.rs @@ -4,6 +4,7 @@ pub mod cli_error; pub mod config; mod did_you_mean; pub mod engine; +pub mod eval_const; mod example; mod exportable; mod id; diff --git a/crates/nu-protocol/src/parse_error.rs b/crates/nu-protocol/src/parse_error.rs index 610f51349..14e2939e1 100644 --- a/crates/nu-protocol/src/parse_error.rs +++ b/crates/nu-protocol/src/parse_error.rs @@ -449,18 +449,6 @@ pub enum ParseError { #[diagnostic(code(nu::shell::error_reading_file))] 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), - help("Only a subset of expressions are allowed constants during parsing. Try using the 'const' command or typing the value literally.") - )] - NotAConstant(#[label = "Value is not a parse-time constant"] Span), - #[error("Invalid literal")] // in . #[diagnostic()] InvalidLiteral(String, String, #[label("{0} in {1}")] Span), @@ -561,7 +549,6 @@ impl ParseError { ParseError::ShellOutErrRedirect(s) => *s, ParseError::UnknownOperator(_, _, s) => *s, ParseError::InvalidLiteral(_, _, s) => *s, - ParseError::NotAConstant(s) => *s, ParseError::LabeledErrorWithHelp { span: s, .. } => *s, } } diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 9dbdce4ab..88cc7f0bc 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -2,7 +2,7 @@ use miette::Diagnostic; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::{ast::Operator, Span, Value}; +use crate::{ast::Operator, engine::StateWorkingSet, format_error, ParseError, Span, Value}; /// The fundamental error type for the evaluation engine. These cases represent different kinds of errors /// the evaluator might face, along with helpful spans to label. An error renderer will take this error value @@ -1074,6 +1074,72 @@ pub enum ShellError { #[label("not a boolean expression")] span: Span, }, + + /// An attempt to run a command marked for constant evaluation lacking the const. eval. + /// implementation. + /// + /// This is an internal Nushell error, please file an issue. + #[error("Missing const eval implementation")] + #[diagnostic( + code(nu::shell::missing_const_eval_implementation), + help( + "The command lacks an implementation for constant evaluation. \ +This is an internal Nushell error, please file an issue https://github.com/nushell/nushell/issues." + ) + )] + MissingConstEvalImpl { + #[label("command lacks constant implementation")] + span: 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::shell::not_a_constant), + help("Only a subset of expressions are allowed constants during parsing. Try using the 'const' command or typing the value literally.") + )] + NotAConstant(#[label = "Value is not a parse-time constant"] Span), + + /// Tried running a command that is not const-compatible + /// + /// ## Resolution + /// + /// Only a subset of builtin commands, and custom commands built only from those commands, can + /// run at parse time. + #[error("Not a const command.")] + #[diagnostic( + code(nu::shell::not_a_const_command), + help("Only a subset of builtin commands, and custom commands built only from those commands, can run at parse time.") + )] + NotAConstCommand(#[label = "This command cannot run at parse time."] Span), + + /// Tried getting a help message at parse time. + /// + /// ## Resolution + /// + /// Help messages are not supported at parse time. + #[error("Help message not a constant.")] + #[diagnostic( + code(nu::shell::not_a_const_help), + help("Help messages are currently not supported to be constants.") + )] + NotAConstHelp(#[label = "Cannot get help message at parse time."] Span), +} + +// TODO: Implement as From trait +impl ShellError { + pub fn wrap(self, working_set: &StateWorkingSet, span: Span) -> ParseError { + let msg = format_error(working_set, &self); + ParseError::LabeledError( + msg, + "Encountered error during parse-time evaluation".into(), + span, + ) + } } impl From for ShellError { diff --git a/src/tests/test_engine.rs b/src/tests/test_engine.rs index 81ca12400..d1933e407 100644 --- a/src/tests/test_engine.rs +++ b/src/tests/test_engine.rs @@ -75,7 +75,7 @@ fn scope_variable() -> TestResult { fn scope_command_defaults(#[case] var: &str, #[case] exp_result: &str) -> TestResult { run_test( &format!( - r#"def t1 [a:int b?:float=1.23 --flag1:string --flag2:float=4.56] {{ true }}; + r#"def t1 [a:int b?:float=1.23 --flag1:string --flag2:float=4.56] {{ true }}; let rslt = (scope commands | where name == 't1' | get signatures.0.any | where parameter_name == '{var}' | get parameter_default.0); $"<($rslt)> ($rslt | describe)""# ), @@ -352,17 +352,14 @@ fn default_value_constant2() -> TestResult { } #[test] -fn default_value_not_constant1() -> TestResult { - fail_test( - r#"def foo [x = ("foo" | str length)] { $x }; foo"#, - "expected a constant", - ) +fn default_value_constant3() -> TestResult { + run_test(r#"def foo [x = ("foo" | str length)] { $x }; foo"#, "3") } #[test] fn default_value_not_constant2() -> TestResult { fail_test( - r#"def foo [--x = ("foo" | str length)] { $x }; foo"#, + r#"def foo [x = (loop { break })] { $x }; foo"#, "expected a constant", ) } diff --git a/tests/const_/mod.rs b/tests/const_/mod.rs index d821e71b9..383f590a4 100644 --- a/tests/const_/mod.rs +++ b/tests/const_/mod.rs @@ -108,12 +108,30 @@ fn const_nothing() { } #[test] -fn const_unsupported() { - let inp = &["const x = ('abc' | str length)"]; +fn const_subexpression_supported() { + let inp = &["const x = ('spam')", "$x"]; let actual = nu!(&inp.join("; ")); - assert!(actual.err.contains("not_a_constant")); + assert_eq!(actual.out, "spam"); +} + +#[test] +fn const_command_supported() { + let inp = &["const x = ('spam' | str length)", "$x"]; + + let actual = nu!(&inp.join("; ")); + + assert_eq!(actual.out, "4"); +} + +#[test] +fn const_command_unsupported() { + let inp = &["const x = (loop { break })"]; + + let actual = nu!(&inp.join("; ")); + + assert!(actual.err.contains("not_a_const_command")); } #[test] @@ -125,6 +143,12 @@ fn const_in_scope() { assert_eq!(actual.out, "x"); } +#[test] +fn not_a_const_help() { + let actual = nu!("const x = ('abc' | str length -h)"); + assert!(actual.err.contains("not_a_const_help")); +} + #[test] fn complex_const_export() { let inp = &[MODULE_SETUP, "use spam", "$spam.X"]; @@ -250,3 +274,22 @@ fn complex_const_overlay_use_hide() { let actual = nu!(&inp.join("; ")); assert!(actual.err.contains("nu::parser::variable_not_found")); } + +// const implementations of commands without dedicated tests +#[test] +fn describe_const() { + let actual = nu!("const x = ('abc' | describe); $x"); + assert_eq!(actual.out, "string"); +} + +#[test] +fn ignore_const() { + let actual = nu!("const x = (echo spam | ignore); $x == null"); + assert_eq!(actual.out, "true"); +} + +#[test] +fn version_const() { + let actual = nu!("const x = (version); $x"); + assert!(actual.err.is_empty()); +}