From 2c3aade0576a1017c644c5ca9537a277d45a1216 Mon Sep 17 00:00:00 2001 From: JT <547158+jntrnr@users.noreply.github.com> Date: Fri, 24 Mar 2023 14:52:01 +1300 Subject: [PATCH] Add pattern matching (#8590) # Description This adds `match` and basic pattern matching. An example: ``` match $x { 1..10 => { print "Value is between 1 and 10" } { foo: $bar } => { print $"Value has a 'foo' field with value ($bar)" } [$a, $b] => { print $"Value is a list with two items: ($a) and ($b)" } _ => { print "Value is none of the above" } } ``` Like the recent changes to `if` to allow it to be used as an expression, `match` can also be used as an expression. This allows you to assign the result to a variable, eg) `let xyz = match ...` I've also included a short-hand pattern for matching records, as I think it might help when doing a lot of record patterns: `{$foo}` which is equivalent to `{foo: $foo}`. There are still missing components, so consider this the first step in full pattern matching support. Currently missing: * Patterns for strings * Or-patterns (like the `|` in Rust) * Patterns for tables (unclear how we want to match a table, so it'll need some design) * Patterns for binary values * And much more # User-Facing Changes [see above] # Tests + Formatting Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` # After Submitting If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. --- crates/nu-cli/src/syntax_highlight.rs | 3 + .../nu-cmd-lang/src/core_commands/match_.rs | 128 +++++++++ crates/nu-cmd-lang/src/core_commands/mod.rs | 2 + crates/nu-cmd-lang/src/default_context.rs | 1 + crates/nu-color-config/src/shape_color.rs | 1 + .../src/database/commands/into_sqlite.rs | 1 + crates/nu-command/src/debug/explain.rs | 1 + crates/nu-command/src/filters/find.rs | 2 + crates/nu-command/src/formats/from/nuon.rs | 12 + crates/nu-command/src/formats/to/json.rs | 5 +- crates/nu-command/src/formats/to/nuon.rs | 6 + crates/nu-command/src/formats/to/text.rs | 1 + crates/nu-command/src/formats/to/toml.rs | 1 + crates/nu-command/src/formats/to/yaml.rs | 1 + crates/nu-engine/src/eval.rs | 5 + crates/nu-parser/src/flatten.rs | 76 +++++- crates/nu-parser/src/lib.rs | 1 + crates/nu-parser/src/parse_patterns.rs | 248 ++++++++++++++++++ crates/nu-parser/src/parser.rs | 129 ++++++++- crates/nu-protocol/src/ast/expr.rs | 4 +- crates/nu-protocol/src/ast/expression.rs | 6 + crates/nu-protocol/src/ast/match_pattern.rs | 21 ++ crates/nu-protocol/src/ast/mod.rs | 2 + crates/nu-protocol/src/engine/mod.rs | 2 + .../nu-protocol/src/engine/pattern_match.rs | 201 ++++++++++++++ crates/nu-protocol/src/syntax_shape.rs | 10 + crates/nu-protocol/src/ty.rs | 4 + crates/nu-protocol/src/value/from_value.rs | 33 ++- crates/nu-protocol/src/value/mod.rs | 53 +++- .../src/sample_config/default_config.nu | 2 + 30 files changed, 955 insertions(+), 7 deletions(-) create mode 100644 crates/nu-cmd-lang/src/core_commands/match_.rs create mode 100644 crates/nu-parser/src/parse_patterns.rs create mode 100644 crates/nu-protocol/src/ast/match_pattern.rs create mode 100644 crates/nu-protocol/src/engine/pattern_match.rs diff --git a/crates/nu-cli/src/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs index 6dacdfba9c..589cc719ba 100644 --- a/crates/nu-cli/src/syntax_highlight.rs +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -126,6 +126,7 @@ impl Highlighter for NuHighlighter { FlatShape::Or => add_colored_token!(shape.1, next_token), FlatShape::Redirection => add_colored_token!(shape.1, next_token), FlatShape::Custom(..) => add_colored_token!(shape.1, next_token), + FlatShape::MatchPattern => add_colored_token!(shape.1, next_token), } last_seen_span = shape.0.end; } @@ -308,6 +309,8 @@ fn find_matching_block_end_in_expr( Expr::ImportPattern(_) => None, Expr::Overlay(_) => None, Expr::Signature(_) => None, + Expr::MatchPattern(_) => None, + Expr::MatchBlock(_) => None, Expr::Nothing => None, Expr::Garbage => None, diff --git a/crates/nu-cmd-lang/src/core_commands/match_.rs b/crates/nu-cmd-lang/src/core_commands/match_.rs new file mode 100644 index 0000000000..6369d173cf --- /dev/null +++ b/crates/nu-cmd-lang/src/core_commands/match_.rs @@ -0,0 +1,128 @@ +use nu_engine::{eval_block, eval_expression_with_input, CallExt}; +use nu_protocol::ast::{Call, Expr, Expression}; +use nu_protocol::engine::{Command, EngineState, Matcher, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, +}; + +#[derive(Clone)] +pub struct Match; + +impl Command for Match { + fn name(&self) -> &str { + "match" + } + + fn usage(&self) -> &str { + "Conditionally run a block on a matched value." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("match") + .input_output_types(vec![(Type::Any, Type::Any)]) + .required("value", SyntaxShape::Any, "value to check") + .required( + "match_block", + SyntaxShape::MatchBlock, + "block to run if check succeeds", + ) + .category(Category::Core) + } + + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn is_parser_keyword(&self) -> bool { + true + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let value: Value = call.req(engine_state, stack, 0)?; + let block = call.positional_nth(1); + + if let Some(Expression { + expr: Expr::MatchBlock(matches), + .. + }) = block + { + for match_ in matches { + let mut match_variables = vec![]; + if match_.0.match_value(&value, &mut match_variables) { + // This case does match, go ahead and return the evaluated expression + for match_variable in match_variables { + stack.add_var(match_variable.0, match_variable.1); + } + + if let Some(block_id) = match_.1.as_block() { + let block = engine_state.get_block(block_id); + return eval_block( + engine_state, + stack, + block, + input, + call.redirect_stdout, + call.redirect_stderr, + ); + } else { + return eval_expression_with_input( + engine_state, + stack, + &match_.1, + input, + call.redirect_stdout, + call.redirect_stderr, + ) + .map(|x| x.0); + } + } + } + } + + Ok(PipelineData::Empty) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Match on a value in range", + example: "match 3 { 1..10 => 'yes!' }", + result: Some(Value::test_string("yes!")), + }, + Example { + description: "Match on a field in a record", + example: "match {a: 100} { {a: $my_value} => { $my_value } }", + result: Some(Value::test_int(100)), + }, + Example { + description: "Match with a catch-all", + example: "match 3 { 1 => { 'yes!' }, _ => { 'no!' } }", + result: Some(Value::test_string("no!")), + }, + Example { + description: "Match against a list", + example: "match [1, 2, 3] { [$a, $b, $c] => { $a + $b + $c }, _ => 0 }", + result: Some(Value::test_int(6)), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Match {}) + } +} diff --git a/crates/nu-cmd-lang/src/core_commands/mod.rs b/crates/nu-cmd-lang/src/core_commands/mod.rs index c55d2812c6..f969026f3f 100644 --- a/crates/nu-cmd-lang/src/core_commands/mod.rs +++ b/crates/nu-cmd-lang/src/core_commands/mod.rs @@ -29,6 +29,7 @@ mod if_; mod ignore; mod let_; mod loop_; +mod match_; mod module; mod mut_; pub(crate) mod overlay; @@ -69,6 +70,7 @@ pub use if_::If; pub use ignore::Ignore; pub use let_::Let; pub use loop_::Loop; +pub use match_::Match; pub use module::Module; pub use mut_::Mut; pub use overlay::*; diff --git a/crates/nu-cmd-lang/src/default_context.rs b/crates/nu-cmd-lang/src/default_context.rs index 71809d634e..81cf90163c 100644 --- a/crates/nu-cmd-lang/src/default_context.rs +++ b/crates/nu-cmd-lang/src/default_context.rs @@ -52,6 +52,7 @@ pub fn create_default_context() -> EngineState { OverlayHide, Let, Loop, + Match, Module, Mut, Return, diff --git a/crates/nu-color-config/src/shape_color.rs b/crates/nu-color-config/src/shape_color.rs index 4186f8a3c2..8fa83c4c6d 100644 --- a/crates/nu-color-config/src/shape_color.rs +++ b/crates/nu-color-config/src/shape_color.rs @@ -23,6 +23,7 @@ pub fn default_shape_color(shape: String) -> Style { "shape_internalcall" => Style::new().fg(Color::Cyan).bold(), "shape_list" => Style::new().fg(Color::Cyan).bold(), "shape_literal" => Style::new().fg(Color::Blue), + "shape_match_pattern" => Style::new().fg(Color::Green), "shape_nothing" => Style::new().fg(Color::LightCyan), "shape_operator" => Style::new().fg(Color::Yellow), "shape_or" => Style::new().fg(Color::Purple).bold(), diff --git a/crates/nu-command/src/database/commands/into_sqlite.rs b/crates/nu-command/src/database/commands/into_sqlite.rs index 033e7b01de..7be28124f5 100644 --- a/crates/nu-command/src/database/commands/into_sqlite.rs +++ b/crates/nu-command/src/database/commands/into_sqlite.rs @@ -270,6 +270,7 @@ fn nu_value_to_string(value: Value, separator: &str) -> String { Value::Binary { val, .. } => format!("{val:?}"), Value::CellPath { val, .. } => val.into_string(), Value::CustomValue { val, .. } => val.value_string(), + Value::MatchPattern { val, .. } => format!("{:?}", val), } } diff --git a/crates/nu-command/src/debug/explain.rs b/crates/nu-command/src/debug/explain.rs index cd10d8a472..a15a2c867e 100644 --- a/crates/nu-command/src/debug/explain.rs +++ b/crates/nu-command/src/debug/explain.rs @@ -328,5 +328,6 @@ pub fn debug_string_without_formatting(value: &Value) -> String { Value::Binary { val, .. } => format!("{val:?}"), Value::CellPath { val, .. } => val.into_string(), Value::CustomValue { val, .. } => val.value_string(), + Value::MatchPattern { val, .. } => format!("{:?}", val), } } diff --git a/crates/nu-command/src/filters/find.rs b/crates/nu-command/src/filters/find.rs index a177d1ea22..f634fb4d82 100644 --- a/crates/nu-command/src/filters/find.rs +++ b/crates/nu-command/src/filters/find.rs @@ -404,6 +404,7 @@ fn find_with_rest_and_highlight( Err(_) => false, }, Value::Binary { .. } => false, + Value::MatchPattern { .. } => false, }) != invert }, ctrlc, @@ -484,6 +485,7 @@ fn find_with_rest_and_highlight( Err(_) => false, }, Value::Binary { .. } => false, + Value::MatchPattern { .. } => false, }) != invert }), ctrlc.clone(), diff --git a/crates/nu-command/src/formats/from/nuon.rs b/crates/nu-command/src/formats/from/nuon.rs index 91a3128c5c..584e87c941 100644 --- a/crates/nu-command/src/formats/from/nuon.rs +++ b/crates/nu-command/src/formats/from/nuon.rs @@ -249,6 +249,12 @@ fn convert_to_value( "extra tokens in input file".into(), expr.span, )), + Expr::MatchPattern(..) => Err(ShellError::OutsideSpannedLabeledError( + original_text.to_string(), + "Error when loading".into(), + "extra tokens in input file".into(), + expr.span, + )), Expr::GlobPattern(val) => Ok(Value::String { val, span }), Expr::ImportPattern(..) => Err(ShellError::OutsideSpannedLabeledError( original_text.to_string(), @@ -277,6 +283,12 @@ fn convert_to_value( Ok(Value::List { vals: output, span }) } + Expr::MatchBlock(..) => Err(ShellError::OutsideSpannedLabeledError( + original_text.to_string(), + "Error when loading".into(), + "match blocks not supported in nuon".into(), + expr.span, + )), Expr::Nothing => Ok(Value::Nothing { span }), Expr::Operator(..) => Err(ShellError::OutsideSpannedLabeledError( original_text.to_string(), diff --git a/crates/nu-command/src/formats/to/json.rs b/crates/nu-command/src/formats/to/json.rs index 2ee30d4bc8..54f4cac74f 100644 --- a/crates/nu-command/src/formats/to/json.rs +++ b/crates/nu-command/src/formats/to/json.rs @@ -127,7 +127,10 @@ pub fn value_to_json_value(v: &Value) -> Result { Value::List { vals, .. } => nu_json::Value::Array(json_list(vals)?), Value::Error { error } => return Err(*error.clone()), - Value::Closure { .. } | Value::Block { .. } | Value::Range { .. } => nu_json::Value::Null, + Value::Closure { .. } + | Value::Block { .. } + | Value::Range { .. } + | Value::MatchPattern { .. } => nu_json::Value::Null, Value::Binary { val, .. } => { nu_json::Value::Array(val.iter().map(|x| nu_json::Value::U64(*x as u64)).collect()) } diff --git a/crates/nu-command/src/formats/to/nuon.rs b/crates/nu-command/src/formats/to/nuon.rs index c99e6b269c..c712fa132e 100644 --- a/crates/nu-command/src/formats/to/nuon.rs +++ b/crates/nu-command/src/formats/to/nuon.rs @@ -246,6 +246,12 @@ pub fn value_to_string( )) } } + Value::MatchPattern { .. } => Err(ShellError::UnsupportedInput( + "match patterns are currently not nuon-compatible".to_string(), + "value originates from here".into(), + span, + v.expect_span(), + )), Value::Nothing { .. } => Ok("null".to_string()), Value::Range { val, .. } => Ok(format!( "{}..{}{}", diff --git a/crates/nu-command/src/formats/to/text.rs b/crates/nu-command/src/formats/to/text.rs index e6b6c3aaf6..41c14f052b 100644 --- a/crates/nu-command/src/formats/to/text.rs +++ b/crates/nu-command/src/formats/to/text.rs @@ -153,6 +153,7 @@ fn local_into_string(value: Value, separator: &str, config: &Config) -> String { Value::Binary { val, .. } => format!("{val:?}"), Value::CellPath { val, .. } => val.into_string(), Value::CustomValue { val, .. } => val.value_string(), + Value::MatchPattern { val, .. } => format!("{:?}", val), } } diff --git a/crates/nu-command/src/formats/to/toml.rs b/crates/nu-command/src/formats/to/toml.rs index 18951eff3c..bf5d370ae9 100644 --- a/crates/nu-command/src/formats/to/toml.rs +++ b/crates/nu-command/src/formats/to/toml.rs @@ -93,6 +93,7 @@ fn helper(engine_state: &EngineState, v: &Value) -> Result, ShellError>>()?, ), Value::CustomValue { .. } => toml::Value::String("".to_string()), + Value::MatchPattern { .. } => toml::Value::String("".to_string()), }) } diff --git a/crates/nu-command/src/formats/to/yaml.rs b/crates/nu-command/src/formats/to/yaml.rs index 49e0d6e24b..648b1e7aba 100644 --- a/crates/nu-command/src/formats/to/yaml.rs +++ b/crates/nu-command/src/formats/to/yaml.rs @@ -97,6 +97,7 @@ pub fn value_to_yaml_value(v: &Value) -> Result { .collect::, ShellError>>()?, ), Value::CustomValue { .. } => serde_yaml::Value::Null, + Value::MatchPattern { .. } => serde_yaml::Value::Null, }) } diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index c951735d6a..f3ad15dd02 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -335,6 +335,11 @@ pub fn eval_expression( span: expr.span, }), Expr::Operator(_) => Ok(Value::Nothing { span: expr.span }), + Expr::MatchPattern(pattern) => Ok(Value::MatchPattern { + val: pattern.clone(), + span: expr.span, + }), + Expr::MatchBlock(_) => Ok(Value::Nothing { span: expr.span }), // match blocks are handled by `match` Expr::UnaryNot(expr) => { let lhs = eval_expression(engine_state, stack, expr)?; match lhs { diff --git a/crates/nu-parser/src/flatten.rs b/crates/nu-parser/src/flatten.rs index 58b30b507c..d9030c37a1 100644 --- a/crates/nu-parser/src/flatten.rs +++ b/crates/nu-parser/src/flatten.rs @@ -1,5 +1,6 @@ use nu_protocol::ast::{ - Block, Expr, Expression, ImportPatternMember, PathMember, Pipeline, PipelineElement, + Block, Expr, Expression, ImportPatternMember, MatchPattern, PathMember, Pattern, Pipeline, + PipelineElement, }; use nu_protocol::DeclId; use nu_protocol::{engine::StateWorkingSet, Span}; @@ -25,6 +26,7 @@ pub enum FlatShape { InternalCall, List, Literal, + MatchPattern, Nothing, Operator, Or, @@ -60,6 +62,7 @@ impl Display for FlatShape { FlatShape::InternalCall => write!(f, "shape_internalcall"), FlatShape::List => write!(f, "shape_list"), FlatShape::Literal => write!(f, "shape_literal"), + FlatShape::MatchPattern => write!(f, "shape_match_pattern"), FlatShape::Nothing => write!(f, "shape_nothing"), FlatShape::Operator => write!(f, "shape_operator"), FlatShape::Or => write!(f, "shape_or"), @@ -212,6 +215,20 @@ pub fn flatten_expression( Expr::Float(_) => { vec![(expr.span, FlatShape::Float)] } + Expr::MatchPattern(pattern) => { + // FIXME: do nicer flattening later + flatten_pattern(pattern) + } + Expr::MatchBlock(matches) => { + let mut output = vec![]; + + for match_ in matches { + output.extend(flatten_pattern(&match_.0)); + output.extend(flatten_expression(working_set, &match_.1)); + } + + output + } Expr::ValueWithUnit(x, unit) => { let mut output = flatten_expression(working_set, x); output.push((unit.span, FlatShape::String)); @@ -488,3 +505,60 @@ pub fn flatten_pipeline( } output } + +pub fn flatten_pattern(match_pattern: &MatchPattern) -> Vec<(Span, FlatShape)> { + let mut output = vec![]; + match &match_pattern.pattern { + Pattern::Garbage => { + output.push((match_pattern.span, FlatShape::Garbage)); + } + Pattern::IgnoreValue => { + output.push((match_pattern.span, FlatShape::Nothing)); + } + Pattern::List(items) => { + if let Some(first) = items.first() { + if let Some(last) = items.last() { + output.push(( + Span::new(match_pattern.span.start, first.span.start), + FlatShape::MatchPattern, + )); + for item in items { + output.extend(flatten_pattern(item)); + } + output.push(( + Span::new(last.span.end, match_pattern.span.end), + FlatShape::MatchPattern, + )) + } + } else { + output.push((match_pattern.span, FlatShape::MatchPattern)); + } + } + Pattern::Record(items) => { + if let Some(first) = items.first() { + if let Some(last) = items.last() { + output.push(( + Span::new(match_pattern.span.start, first.1.span.start), + FlatShape::MatchPattern, + )); + for item in items { + output.extend(flatten_pattern(&item.1)); + } + output.push(( + Span::new(last.1.span.end, match_pattern.span.end), + FlatShape::MatchPattern, + )) + } + } else { + output.push((match_pattern.span, FlatShape::MatchPattern)); + } + } + Pattern::Value(_) => { + output.push((match_pattern.span, FlatShape::MatchPattern)); + } + Pattern::Variable(_) => { + output.push((match_pattern.span, FlatShape::Variable)); + } + } + output +} diff --git a/crates/nu-parser/src/lib.rs b/crates/nu-parser/src/lib.rs index 4ab293c322..018e00f2aa 100644 --- a/crates/nu-parser/src/lib.rs +++ b/crates/nu-parser/src/lib.rs @@ -6,6 +6,7 @@ mod known_external; mod lex; mod lite_parser; mod parse_keywords; +mod parse_patterns; mod parser; mod type_check; diff --git a/crates/nu-parser/src/parse_patterns.rs b/crates/nu-parser/src/parse_patterns.rs new file mode 100644 index 0000000000..fa988cea96 --- /dev/null +++ b/crates/nu-parser/src/parse_patterns.rs @@ -0,0 +1,248 @@ +use nu_protocol::{ + ast::{Expr, Expression, MatchPattern, Pattern}, + engine::StateWorkingSet, + Span, SyntaxShape, Type, +}; + +use crate::{ + lex, lite_parse, + parser::{is_variable, parse_value}, + LiteElement, ParseError, +}; + +pub fn garbage(span: Span) -> MatchPattern { + MatchPattern { + pattern: Pattern::Garbage, + span, + } +} + +pub fn parse_match_pattern( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + working_set.enter_scope(); + let (output, err) = parse_pattern(working_set, span); + working_set.exit_scope(); + + ( + Expression { + expr: Expr::MatchPattern(Box::new(output)), + span, + ty: Type::Any, + custom_completion: None, + }, + err, + ) +} + +pub fn parse_pattern( + working_set: &mut StateWorkingSet, + span: Span, +) -> (MatchPattern, Option) { + let bytes = working_set.get_span_contents(span); + + if bytes.starts_with(b"$") { + // Variable pattern + parse_variable_pattern(working_set, span) + } else if bytes.starts_with(b"{") { + // Record pattern + parse_record_pattern(working_set, span) + } else if bytes.starts_with(b"[") { + // List pattern + parse_list_pattern(working_set, span) + } else if bytes == b"_" { + ( + MatchPattern { + pattern: Pattern::IgnoreValue, + span, + }, + None, + ) + } else { + // Literal value + let (value, error) = parse_value(working_set, span, &SyntaxShape::Any, &[]); + ( + MatchPattern { + pattern: Pattern::Value(value), + span, + }, + error, + ) + } +} + +pub fn parse_variable_pattern( + working_set: &mut StateWorkingSet, + span: Span, +) -> (MatchPattern, Option) { + let bytes = working_set.get_span_contents(span); + + if is_variable(bytes) { + if let Some(var_id) = working_set.find_variable(bytes) { + ( + MatchPattern { + pattern: Pattern::Variable(var_id), + span, + }, + None, + ) + } else { + let var_id = working_set.add_variable(bytes.to_vec(), span, Type::Any, true); + + ( + MatchPattern { + pattern: Pattern::Variable(var_id), + span, + }, + None, + ) + } + } else { + ( + garbage(span), + Some(ParseError::Expected("valid variable name".into(), span)), + ) + } +} + +pub fn parse_list_pattern( + working_set: &mut StateWorkingSet, + span: Span, +) -> (MatchPattern, Option) { + let bytes = working_set.get_span_contents(span); + + let mut error = None; + + let mut start = span.start; + let mut end = span.end; + + if bytes.starts_with(b"[") { + start += 1; + } + if bytes.ends_with(b"]") { + end -= 1; + } else { + error = error.or_else(|| Some(ParseError::Unclosed("]".into(), Span::new(end, end)))); + } + + let inner_span = Span::new(start, end); + let source = working_set.get_span_contents(inner_span); + + let (output, err) = lex(source, inner_span.start, &[b'\n', b'\r', b','], &[], true); + error = error.or(err); + + let (output, err) = lite_parse(&output); + error = error.or(err); + + let mut args = vec![]; + + if !output.block.is_empty() { + for arg in &output.block[0].commands { + let mut spans_idx = 0; + + if let LiteElement::Command(_, command) = arg { + while spans_idx < command.parts.len() { + let (arg, err) = parse_pattern(working_set, command.parts[spans_idx]); + error = error.or(err); + + args.push(arg); + + spans_idx += 1; + } + } + } + } + + ( + MatchPattern { + pattern: Pattern::List(args), + span, + }, + error, + ) +} + +pub fn parse_record_pattern( + working_set: &mut StateWorkingSet, + span: Span, +) -> (MatchPattern, Option) { + let bytes = working_set.get_span_contents(span); + + let mut error = None; + let mut start = span.start; + let mut end = span.end; + + if bytes.starts_with(b"{") { + start += 1; + } else { + error = error.or_else(|| { + Some(ParseError::Expected( + "{".into(), + Span::new(start, start + 1), + )) + }); + } + + if bytes.ends_with(b"}") { + end -= 1; + } else { + error = error.or_else(|| Some(ParseError::Unclosed("}".into(), Span::new(end, end)))); + } + + let inner_span = Span::new(start, end); + let source = working_set.get_span_contents(inner_span); + + let (tokens, err) = lex(source, start, &[b'\n', b'\r', b','], &[b':'], true); + error = error.or(err); + + let mut output = vec![]; + let mut idx = 0; + + while idx < tokens.len() { + let bytes = working_set.get_span_contents(tokens[idx].span); + let (field, pattern) = if !bytes.is_empty() && bytes[0] == b'$' { + // If this is a variable, treat it as both the name of the field and the pattern + let field = String::from_utf8_lossy(&bytes[1..]).to_string(); + + let (pattern, err) = parse_variable_pattern(working_set, tokens[idx].span); + error = error.or(err); + + (field, pattern) + } else { + let field = String::from_utf8_lossy(bytes).to_string(); + + idx += 1; + if idx == tokens.len() { + return ( + garbage(span), + Some(ParseError::Expected("record".into(), span)), + ); + } + let colon = working_set.get_span_contents(tokens[idx].span); + idx += 1; + if idx == tokens.len() || colon != b":" { + //FIXME: need better error + return ( + garbage(span), + Some(ParseError::Expected("record".into(), span)), + ); + } + let (pattern, err) = parse_pattern(working_set, tokens[idx].span); + error = error.or(err); + + (field, pattern) + }; + idx += 1; + + output.push((field, pattern)); + } + + ( + MatchPattern { + pattern: Pattern::Record(output), + span, + }, + error, + ) +} diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 4443f77ce0..1e0e445f71 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -3,6 +3,7 @@ use crate::{ lex, lite_parser::{lite_parse, LiteCommand, LiteElement}, parse_mut, + parse_patterns::{parse_match_pattern, parse_pattern}, type_check::{math_result_type, type_compatible}, ParseError, Token, TokenContents, }; @@ -76,6 +77,7 @@ pub fn is_math_expression_like( || bytes == b"null" || bytes == b"not" || bytes == b"if" + || bytes == b"match" { return true; } @@ -120,7 +122,7 @@ fn is_identifier(bytes: &[u8]) -> bool { bytes.iter().all(|x| is_identifier_byte(*x)) } -fn is_variable(bytes: &[u8]) -> bool { +pub fn is_variable(bytes: &[u8]) -> bool { if bytes.len() > 1 && bytes[0] == b'$' { is_identifier(&bytes[1..]) } else { @@ -1787,6 +1789,8 @@ pub fn parse_brace_expr( parse_closure_expression(working_set, shape, span, expand_aliases_denylist, true) } else if matches!(shape, SyntaxShape::Block) { parse_block_expression(working_set, span, expand_aliases_denylist) + } else if matches!(shape, SyntaxShape::MatchBlock) { + parse_match_block_expression(working_set, span, expand_aliases_denylist) } else { parse_record(working_set, span, expand_aliases_denylist) } @@ -1802,6 +1806,8 @@ pub fn parse_brace_expr( parse_closure_expression(working_set, shape, span, expand_aliases_denylist, true) } else if matches!(shape, SyntaxShape::Block) { parse_block_expression(working_set, span, expand_aliases_denylist) + } else if matches!(shape, SyntaxShape::MatchBlock) { + parse_match_block_expression(working_set, span, expand_aliases_denylist) } else { ( Expression::garbage(span), @@ -4415,6 +4421,113 @@ pub fn parse_block_expression( ) } +pub fn parse_match_block_expression( + working_set: &mut StateWorkingSet, + span: Span, + expand_aliases_denylist: &[usize], +) -> (Expression, Option) { + let bytes = working_set.get_span_contents(span); + let mut error = None; + + let mut start = span.start; + let mut end = span.end; + + if bytes.starts_with(b"{") { + start += 1; + } else { + return ( + garbage(span), + Some(ParseError::Expected("closure".into(), span)), + ); + } + if bytes.ends_with(b"}") { + end -= 1; + } else { + error = error.or_else(|| Some(ParseError::Unclosed("}".into(), Span::new(end, end)))); + } + + let inner_span = Span::new(start, end); + + let source = working_set.get_span_contents(inner_span); + + let (output, err) = lex(source, start, &[b' ', b'\r', b'\n', b','], &[], false); + error = error.or(err); + + let mut position = 0; + + let mut output_matches = vec![]; + + while position < output.len() { + // Each match gets its own scope + + working_set.enter_scope(); + + // First parse the pattern + let (pattern, err) = parse_pattern(working_set, output[position].span); + error = error.or(err); + + position += 1; + + if position >= output.len() { + error = error.or(Some(ParseError::Mismatch( + "=>".into(), + "end of input".into(), + Span::new(output[position - 1].span.end, output[position - 1].span.end), + ))); + + working_set.exit_scope(); + break; + } + + // Then the => + let thick_arrow = working_set.get_span_contents(output[position].span); + if thick_arrow != b"=>" { + error = error.or(Some(ParseError::Mismatch( + "=>".into(), + "end of input".into(), + Span::new(output[position - 1].span.end, output[position - 1].span.end), + ))); + } + + // Finally, the value/expression/block that we will run to produce the result + position += 1; + + if position >= output.len() { + error = error.or(Some(ParseError::Mismatch( + "match result".into(), + "end of input".into(), + Span::new(output[position - 1].span.end, output[position - 1].span.end), + ))); + + working_set.exit_scope(); + break; + } + + let (result, err) = parse_multispan_value( + working_set, + &[output[position].span], + &mut 0, + &SyntaxShape::OneOf(vec![SyntaxShape::Block, SyntaxShape::Expression]), + expand_aliases_denylist, + ); + error = error.or(err); + position += 1; + working_set.exit_scope(); + + output_matches.push((pattern, result)); + } + + ( + Expression { + expr: Expr::MatchBlock(output_matches), + span, + ty: Type::Any, + custom_completion: None, + }, + error, + ) +} + pub fn parse_closure_expression( working_set: &mut StateWorkingSet, shape: &SyntaxShape, @@ -4650,6 +4763,10 @@ pub fn parse_value( _ => {} } + if matches!(shape, SyntaxShape::MatchPattern) { + return parse_match_pattern(working_set, span); + } + match bytes[0] { b'$' => return parse_dollar_expr(working_set, span, expand_aliases_denylist), b'(' => return parse_paren_expr(working_set, span, shape, expand_aliases_denylist), @@ -4688,6 +4805,7 @@ pub fn parse_value( SyntaxShape::GlobPattern => parse_glob_pattern(working_set, span), SyntaxShape::String => parse_string(working_set, span, expand_aliases_denylist), SyntaxShape::Binary => parse_binary(working_set, span), + SyntaxShape::MatchPattern => parse_match_pattern(working_set, span), SyntaxShape::Signature => { if bytes.starts_with(b"[") { parse_signature(working_set, span, expand_aliases_denylist) @@ -4998,7 +5116,7 @@ pub fn parse_math_expression( let first_span = working_set.get_span_contents(spans[0]); - if first_span == b"if" { + if first_span == b"if" || first_span == b"match" { // If expression if spans.len() > 1 { return parse_call(working_set, spans, spans[0], expand_aliases_denylist, false); @@ -6085,6 +6203,13 @@ pub fn discover_captures_in_expr( output.extend(&result); } } + Expr::MatchPattern(_) => {} + Expr::MatchBlock(match_block) => { + for match_ in match_block { + let result = discover_captures_in_expr(working_set, &match_.1, seen, seen_blocks)?; + output.extend(&result); + } + } Expr::RowCondition(block_id) | Expr::Subexpression(block_id) => { let block = working_set.get_block(*block_id); let results = { diff --git a/crates/nu-protocol/src/ast/expr.rs b/crates/nu-protocol/src/ast/expr.rs index 24f69c8ed5..4af9a1650e 100644 --- a/crates/nu-protocol/src/ast/expr.rs +++ b/crates/nu-protocol/src/ast/expr.rs @@ -1,7 +1,7 @@ use chrono::FixedOffset; use serde::{Deserialize, Serialize}; -use super::{Call, CellPath, Expression, FullCellPath, Operator, RangeOperator}; +use super::{Call, CellPath, Expression, FullCellPath, MatchPattern, Operator, RangeOperator}; use crate::{ast::ImportPattern, BlockId, Signature, Span, Spanned, Unit, VarId}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -27,6 +27,7 @@ pub enum Expr { Subexpression(BlockId), Block(BlockId), Closure(BlockId), + MatchBlock(Vec<(MatchPattern, Expression)>), List(Vec), Table(Vec, Vec>), Record(Vec<(Expression, Expression)>), @@ -43,6 +44,7 @@ pub enum Expr { Overlay(Option), // block ID of the overlay's origin module Signature(Box), StringInterpolation(Vec), + MatchPattern(Box), Nothing, Garbage, } diff --git a/crates/nu-protocol/src/ast/expression.rs b/crates/nu-protocol/src/ast/expression.rs index 2d224f99c8..e20b105dcd 100644 --- a/crates/nu-protocol/src/ast/expression.rs +++ b/crates/nu-protocol/src/ast/expression.rs @@ -221,7 +221,9 @@ impl Expression { } false } + Expr::MatchPattern(_) => false, Expr::Operator(_) => false, + Expr::MatchBlock(_) => false, Expr::Range(left, middle, right, ..) => { if let Some(left) = &left { if left.has_in_variable(working_set) { @@ -395,6 +397,8 @@ impl Expression { Expr::Nothing => {} Expr::GlobPattern(_) => {} Expr::Int(_) => {} + Expr::MatchPattern(_) => {} + Expr::MatchBlock(_) => {} Expr::Keyword(_, _, expr) => expr.replace_in_variable(working_set, new_var_id), Expr::List(list) => { for l in list { @@ -554,6 +558,8 @@ impl Expression { Expr::Garbage => {} Expr::Nothing => {} Expr::GlobPattern(_) => {} + Expr::MatchPattern(_) => {} + Expr::MatchBlock(_) => {} Expr::Int(_) => {} Expr::Keyword(_, _, expr) => expr.replace_span(working_set, replaced, new_span), Expr::List(list) => { diff --git a/crates/nu-protocol/src/ast/match_pattern.rs b/crates/nu-protocol/src/ast/match_pattern.rs new file mode 100644 index 0000000000..9428eddb0c --- /dev/null +++ b/crates/nu-protocol/src/ast/match_pattern.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +use crate::{Span, VarId}; + +use super::Expression; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct MatchPattern { + pub pattern: Pattern, + pub span: Span, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Pattern { + Record(Vec<(String, MatchPattern)>), + List(Vec), + Value(Expression), + Variable(VarId), + IgnoreValue, // the _ pattern + Garbage, +} diff --git a/crates/nu-protocol/src/ast/mod.rs b/crates/nu-protocol/src/ast/mod.rs index f34cf24810..a0b12d643b 100644 --- a/crates/nu-protocol/src/ast/mod.rs +++ b/crates/nu-protocol/src/ast/mod.rs @@ -4,6 +4,7 @@ mod cell_path; mod expr; mod expression; mod import_pattern; +mod match_pattern; mod operator; mod pipeline; @@ -13,5 +14,6 @@ pub use cell_path::*; pub use expr::*; pub use expression::*; pub use import_pattern::*; +pub use match_pattern::*; pub use operator::*; pub use pipeline::*; diff --git a/crates/nu-protocol/src/engine/mod.rs b/crates/nu-protocol/src/engine/mod.rs index 5eab7c84a3..bb9620af40 100644 --- a/crates/nu-protocol/src/engine/mod.rs +++ b/crates/nu-protocol/src/engine/mod.rs @@ -3,6 +3,7 @@ mod capture_block; mod command; mod engine_state; mod overlay; +mod pattern_match; mod stack; pub use call_info::*; @@ -10,4 +11,5 @@ pub use capture_block::*; pub use command::*; pub use engine_state::*; pub use overlay::*; +pub use pattern_match::*; pub use stack::*; diff --git a/crates/nu-protocol/src/engine/pattern_match.rs b/crates/nu-protocol/src/engine/pattern_match.rs new file mode 100644 index 0000000000..3f1ff3bc5c --- /dev/null +++ b/crates/nu-protocol/src/engine/pattern_match.rs @@ -0,0 +1,201 @@ +use crate::{ + ast::{Expr, MatchPattern, Pattern, RangeInclusion}, + Unit, Value, VarId, +}; + +pub trait Matcher { + fn match_value(&self, value: &Value, matches: &mut Vec<(VarId, Value)>) -> bool; +} + +impl Matcher for MatchPattern { + fn match_value(&self, value: &Value, matches: &mut Vec<(VarId, Value)>) -> bool { + self.pattern.match_value(value, matches) + } +} + +impl Matcher for Pattern { + fn match_value(&self, value: &Value, matches: &mut Vec<(VarId, Value)>) -> bool { + match self { + Pattern::Garbage => false, + Pattern::IgnoreValue => true, + Pattern::Record(field_patterns) => match value { + Value::Record { cols, vals, .. } => { + 'top: for field_pattern in field_patterns { + for (col_idx, col) in cols.iter().enumerate() { + if col == &field_pattern.0 { + // We have found the field + let result = field_pattern.1.match_value(&vals[col_idx], matches); + if !result { + return false; + } else { + continue 'top; + } + } + } + return false; + } + true + } + _ => false, + }, + Pattern::Variable(var_id) => { + // TODO: FIXME: This needs the span of this variable + matches.push((*var_id, value.clone())); + true + } + Pattern::List(items) => match &value { + Value::List { vals, .. } => { + if items.len() > vals.len() { + // We need more items in our pattern than are available in the Value + return false; + } + + for (val_idx, val) in vals.iter().enumerate() { + // We require that the pattern and the value have the same number of items, or the pattern does not match + // The only exception is if the pattern includes a `..` pattern + + if let Some(pattern) = items.get(val_idx) { + if !pattern.match_value(val, matches) { + return false; + } + } else { + return false; + } + } + + true + } + _ => false, + }, + Pattern::Value(pattern_value) => { + // TODO: Fill this out with the rest of them + match &pattern_value.expr { + Expr::Int(x) => { + if let Value::Int { val, .. } = &value { + x == val + } else { + false + } + } + Expr::Binary(x) => { + if let Value::Binary { val, .. } = &value { + x == val + } else { + false + } + } + Expr::Bool(x) => { + if let Value::Bool { val, .. } = &value { + x == val + } else { + false + } + } + Expr::ValueWithUnit(amount, unit) => { + if let Value::Filesize { val, .. } = &value { + // FIXME: we probably want this math in one place that both the + // pattern matcher and the eval engine can get to it + match &amount.expr { + Expr::Int(amount) => match &unit.item { + Unit::Byte => amount == val, + Unit::Kilobyte => *val == amount * 1000, + Unit::Megabyte => *val == amount * 1000 * 1000, + Unit::Gigabyte => *val == amount * 1000 * 1000 * 1000, + Unit::Petabyte => *val == amount * 1000 * 1000 * 1000 * 1000, + Unit::Exabyte => { + *val == amount * 1000 * 1000 * 1000 * 1000 * 1000 + } + Unit::Zettabyte => { + *val == amount * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 + } + Unit::Kibibyte => *val == amount * 1024, + Unit::Mebibyte => *val == amount * 1024 * 1024, + Unit::Gibibyte => *val == amount * 1024 * 1024 * 1024, + Unit::Pebibyte => *val == amount * 1024 * 1024 * 1024 * 1024, + Unit::Exbibyte => { + *val == amount * 1024 * 1024 * 1024 * 1024 * 1024 + } + Unit::Zebibyte => { + *val == amount * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + } + _ => false, + }, + _ => false, + } + } else if let Value::Duration { val, .. } = &value { + // FIXME: we probably want this math in one place that both the + // pattern matcher and the eval engine can get to it + match &amount.expr { + Expr::Int(amount) => match &unit.item { + Unit::Nanosecond => val == amount, + Unit::Microsecond => *val == amount * 1000, + Unit::Millisecond => *val == amount * 1000 * 1000, + Unit::Second => *val == amount * 1000 * 1000 * 1000, + Unit::Minute => *val == amount * 1000 * 1000 * 1000 * 60, + Unit::Hour => *val == amount * 1000 * 1000 * 1000 * 60 * 60, + Unit::Day => *val == amount * 1000 * 1000 * 1000 * 60 * 60 * 24, + Unit::Week => { + *val == amount * 1000 * 1000 * 1000 * 60 * 60 * 24 * 7 + } + _ => false, + }, + _ => false, + } + } else { + false + } + } + Expr::Range(start, step, end, inclusion) => { + // TODO: Add support for floats + + let start = if let Some(start) = &start { + match &start.expr { + Expr::Int(start) => *start, + _ => return false, + } + } else { + 0 + }; + + let end = if let Some(end) = &end { + match &end.expr { + Expr::Int(end) => *end, + _ => return false, + } + } else { + i64::MAX + }; + + let step = if let Some(step) = step { + match &step.expr { + Expr::Int(step) => *step - start, + _ => return false, + } + } else if end < start { + -1 + } else { + 1 + }; + + let (start, end) = if end < start { + (end, start) + } else { + (start, end) + }; + + if let Value::Int { val, .. } = &value { + if matches!(inclusion.inclusion, RangeInclusion::RightExclusive) { + *val >= start && *val < end && ((*val - start) % step) == 0 + } else { + *val >= start && *val <= end && ((*val - start) % step) == 0 + } + } else { + false + } + } + _ => false, + } + } + } + } +} diff --git a/crates/nu-protocol/src/syntax_shape.rs b/crates/nu-protocol/src/syntax_shape.rs index fb96d45e66..ab1fff61dc 100644 --- a/crates/nu-protocol/src/syntax_shape.rs +++ b/crates/nu-protocol/src/syntax_shape.rs @@ -73,6 +73,12 @@ pub enum SyntaxShape { /// A general math expression, eg `1 + 2` MathExpression, + /// A block of matches, used by `match` + MatchBlock, + + /// A match pattern, eg `{a: $foo}` + MatchPattern, + /// Nothing Nothing, @@ -137,6 +143,8 @@ impl SyntaxShape { Type::List(Box::new(contents)) } SyntaxShape::Keyword(_, expr) => expr.to_type(), + SyntaxShape::MatchBlock => Type::Any, + SyntaxShape::MatchPattern => Type::Any, SyntaxShape::MathExpression => Type::Any, SyntaxShape::Nothing => Type::Any, SyntaxShape::Number => Type::Number, @@ -196,6 +204,8 @@ impl Display for SyntaxShape { SyntaxShape::Variable => write!(f, "var"), SyntaxShape::VarWithOptType => write!(f, "vardecl"), SyntaxShape::Signature => write!(f, "signature"), + SyntaxShape::MatchPattern => write!(f, "matchpattern"), + SyntaxShape::MatchBlock => write!(f, "matchblock"), SyntaxShape::Expression => write!(f, "expression"), SyntaxShape::Boolean => write!(f, "bool"), SyntaxShape::Error => write!(f, "error"), diff --git a/crates/nu-protocol/src/ty.rs b/crates/nu-protocol/src/ty.rs index 4c73f3124b..754afd88d0 100644 --- a/crates/nu-protocol/src/ty.rs +++ b/crates/nu-protocol/src/ty.rs @@ -22,6 +22,7 @@ pub enum Type { Int, List(Box), ListStream, + MatchPattern, #[default] Nothing, Number, @@ -94,6 +95,7 @@ impl Type { Type::Binary => SyntaxShape::Binary, Type::Custom(_) => SyntaxShape::Any, Type::Signature => SyntaxShape::Signature, + Type::MatchPattern => SyntaxShape::MatchPattern, } } @@ -114,6 +116,7 @@ impl Type { Type::Record(_) => String::from("record"), Type::Table(_) => String::from("table"), Type::List(_) => String::from("list"), + Type::MatchPattern => String::from("match pattern"), Type::Nothing => String::from("nothing"), Type::Number => String::from("number"), Type::String => String::from("string"), @@ -180,6 +183,7 @@ impl Display for Type { Type::Binary => write!(f, "binary"), Type::Custom(custom) => write!(f, "{custom}"), Type::Signature => write!(f, "signature"), + Type::MatchPattern => write!(f, "match pattern"), } } } diff --git a/crates/nu-protocol/src/value/from_value.rs b/crates/nu-protocol/src/value/from_value.rs index 89ac2926b4..c69e188e66 100644 --- a/crates/nu-protocol/src/value/from_value.rs +++ b/crates/nu-protocol/src/value/from_value.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::path::PathBuf; use std::str::FromStr; -use crate::ast::{CellPath, PathMember}; +use crate::ast::{CellPath, MatchPattern, PathMember}; use crate::engine::{Block, Closure}; use crate::ShellError; use crate::{Range, Spanned, Value}; @@ -563,3 +563,34 @@ impl FromValue for Spanned { } } } + +impl FromValue for Spanned { + fn from_value(v: &Value) -> Result { + match v { + Value::MatchPattern { val, span } => Ok(Spanned { + item: *val.clone(), + span: *span, + }), + v => Err(ShellError::CantConvert { + to_type: "Match pattern".into(), + from_type: v.get_type().to_string(), + span: v.span()?, + help: None, + }), + } + } +} + +impl FromValue for MatchPattern { + fn from_value(v: &Value) -> Result { + match v { + Value::MatchPattern { val, .. } => Ok(*val.clone()), + v => Err(ShellError::CantConvert { + to_type: "Match pattern".into(), + from_type: v.get_type().to_string(), + span: v.span()?, + help: None, + }), + } + } +} diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index c4cb313d93..3f20f07900 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -6,7 +6,7 @@ mod range; mod stream; mod unit; -use crate::ast::{Bits, Boolean, CellPath, Comparison, PathMember}; +use crate::ast::{Bits, Boolean, CellPath, Comparison, MatchPattern, PathMember}; use crate::ast::{Math, Operator}; use crate::engine::EngineState; use crate::ShellError; @@ -113,6 +113,10 @@ pub enum Value { val: Box, span: Span, }, + MatchPattern { + val: Box, + span: Span, + }, } impl Clone for Value { @@ -185,6 +189,10 @@ impl Clone for Value { span: *span, }, Value::CustomValue { val, span } => val.clone_value(*span), + Value::MatchPattern { val, span } => Value::MatchPattern { + val: val.clone(), + span: *span, + }, } } } @@ -387,6 +395,7 @@ impl Value { Value::CellPath { span, .. } => Ok(*span), Value::CustomValue { span, .. } => Ok(*span), Value::LazyRecord { span, .. } => Ok(*span), + Value::MatchPattern { span, .. } => Ok(*span), } } @@ -418,6 +427,7 @@ impl Value { Value::Binary { span, .. } => *span = new_span, Value::CellPath { span, .. } => *span = new_span, Value::CustomValue { span, .. } => *span = new_span, + Value::MatchPattern { span, .. } => *span = new_span, } self @@ -475,6 +485,7 @@ impl Value { Value::Binary { .. } => Type::Binary, Value::CellPath { .. } => Type::CellPath, Value::CustomValue { val, .. } => Type::Custom(val.typetag_name().into()), + Value::MatchPattern { .. } => Type::MatchPattern, } } @@ -571,6 +582,7 @@ impl Value { Value::Binary { val, .. } => format!("{val:?}"), Value::CellPath { val, .. } => val.into_string(), Value::CustomValue { val, .. } => val.value_string(), + Value::MatchPattern { val, .. } => format!("", val), } } @@ -622,6 +634,7 @@ impl Value { Value::Binary { val, .. } => format!("{val:?}"), Value::CellPath { val, .. } => val.into_string(), Value::CustomValue { val, .. } => val.value_string(), + Value::MatchPattern { .. } => "".into(), } } @@ -673,6 +686,7 @@ impl Value { Value::Binary { val, .. } => format!("{val:?}"), Value::CellPath { val, .. } => val.into_string(), Value::CustomValue { val, .. } => val.value_string(), + Value::MatchPattern { val, .. } => format!("", val), } } @@ -1720,6 +1734,7 @@ impl PartialOrd for Value { Value::Binary { .. } => Some(Ordering::Less), Value::CellPath { .. } => Some(Ordering::Less), Value::CustomValue { .. } => Some(Ordering::Less), + Value::MatchPattern { .. } => Some(Ordering::Less), }, (Value::Int { val: lhs, .. }, rhs) => match rhs { Value::Bool { .. } => Some(Ordering::Greater), @@ -1740,6 +1755,7 @@ impl PartialOrd for Value { Value::Binary { .. } => Some(Ordering::Less), Value::CellPath { .. } => Some(Ordering::Less), Value::CustomValue { .. } => Some(Ordering::Less), + Value::MatchPattern { .. } => Some(Ordering::Less), }, (Value::Float { val: lhs, .. }, rhs) => match rhs { Value::Bool { .. } => Some(Ordering::Greater), @@ -1760,6 +1776,7 @@ impl PartialOrd for Value { Value::Binary { .. } => Some(Ordering::Less), Value::CellPath { .. } => Some(Ordering::Less), Value::CustomValue { .. } => Some(Ordering::Less), + Value::MatchPattern { .. } => Some(Ordering::Less), }, (Value::Filesize { val: lhs, .. }, rhs) => match rhs { Value::Bool { .. } => Some(Ordering::Greater), @@ -1780,6 +1797,7 @@ impl PartialOrd for Value { Value::Binary { .. } => Some(Ordering::Less), Value::CellPath { .. } => Some(Ordering::Less), Value::CustomValue { .. } => Some(Ordering::Less), + Value::MatchPattern { .. } => Some(Ordering::Less), }, (Value::Duration { val: lhs, .. }, rhs) => match rhs { Value::Bool { .. } => Some(Ordering::Greater), @@ -1800,6 +1818,7 @@ impl PartialOrd for Value { Value::Binary { .. } => Some(Ordering::Less), Value::CellPath { .. } => Some(Ordering::Less), Value::CustomValue { .. } => Some(Ordering::Less), + Value::MatchPattern { .. } => Some(Ordering::Less), }, (Value::Date { val: lhs, .. }, rhs) => match rhs { Value::Bool { .. } => Some(Ordering::Greater), @@ -1820,6 +1839,7 @@ impl PartialOrd for Value { Value::Binary { .. } => Some(Ordering::Less), Value::CellPath { .. } => Some(Ordering::Less), Value::CustomValue { .. } => Some(Ordering::Less), + Value::MatchPattern { .. } => Some(Ordering::Less), }, (Value::Range { val: lhs, .. }, rhs) => match rhs { Value::Bool { .. } => Some(Ordering::Greater), @@ -1840,6 +1860,7 @@ impl PartialOrd for Value { Value::Binary { .. } => Some(Ordering::Less), Value::CellPath { .. } => Some(Ordering::Less), Value::CustomValue { .. } => Some(Ordering::Less), + Value::MatchPattern { .. } => Some(Ordering::Less), }, (Value::String { val: lhs, .. }, rhs) => match rhs { Value::Bool { .. } => Some(Ordering::Greater), @@ -1860,6 +1881,7 @@ impl PartialOrd for Value { Value::Binary { .. } => Some(Ordering::Less), Value::CellPath { .. } => Some(Ordering::Less), Value::CustomValue { .. } => Some(Ordering::Less), + Value::MatchPattern { .. } => Some(Ordering::Less), }, ( Value::Record { @@ -1912,6 +1934,7 @@ impl PartialOrd for Value { Value::Binary { .. } => Some(Ordering::Less), Value::CellPath { .. } => Some(Ordering::Less), Value::CustomValue { .. } => Some(Ordering::Less), + Value::MatchPattern { .. } => Some(Ordering::Less), }, (Value::List { vals: lhs, .. }, rhs) => match rhs { Value::Bool { .. } => Some(Ordering::Greater), @@ -1932,6 +1955,7 @@ impl PartialOrd for Value { Value::Binary { .. } => Some(Ordering::Less), Value::CellPath { .. } => Some(Ordering::Less), Value::CustomValue { .. } => Some(Ordering::Less), + Value::MatchPattern { .. } => Some(Ordering::Less), }, (Value::Block { val: lhs, .. }, rhs) => match rhs { Value::Bool { .. } => Some(Ordering::Greater), @@ -1952,6 +1976,7 @@ impl PartialOrd for Value { Value::Binary { .. } => Some(Ordering::Less), Value::CellPath { .. } => Some(Ordering::Less), Value::CustomValue { .. } => Some(Ordering::Less), + Value::MatchPattern { .. } => Some(Ordering::Less), }, (Value::Closure { val: lhs, .. }, rhs) => match rhs { Value::Bool { .. } => Some(Ordering::Greater), @@ -1972,6 +1997,7 @@ impl PartialOrd for Value { Value::Binary { .. } => Some(Ordering::Less), Value::CellPath { .. } => Some(Ordering::Less), Value::CustomValue { .. } => Some(Ordering::Less), + Value::MatchPattern { .. } => Some(Ordering::Less), }, (Value::Nothing { .. }, rhs) => match rhs { Value::Bool { .. } => Some(Ordering::Greater), @@ -1992,6 +2018,7 @@ impl PartialOrd for Value { Value::Binary { .. } => Some(Ordering::Less), Value::CellPath { .. } => Some(Ordering::Less), Value::CustomValue { .. } => Some(Ordering::Less), + Value::MatchPattern { .. } => Some(Ordering::Less), }, (Value::Error { .. }, rhs) => match rhs { Value::Bool { .. } => Some(Ordering::Greater), @@ -2012,6 +2039,7 @@ impl PartialOrd for Value { Value::Binary { .. } => Some(Ordering::Less), Value::CellPath { .. } => Some(Ordering::Less), Value::CustomValue { .. } => Some(Ordering::Less), + Value::MatchPattern { .. } => Some(Ordering::Less), }, (Value::Binary { val: lhs, .. }, rhs) => match rhs { Value::Bool { .. } => Some(Ordering::Greater), @@ -2032,6 +2060,7 @@ impl PartialOrd for Value { Value::Binary { val: rhs, .. } => lhs.partial_cmp(rhs), Value::CellPath { .. } => Some(Ordering::Less), Value::CustomValue { .. } => Some(Ordering::Less), + Value::MatchPattern { .. } => Some(Ordering::Less), }, (Value::CellPath { val: lhs, .. }, rhs) => match rhs { Value::Bool { .. } => Some(Ordering::Greater), @@ -2052,6 +2081,7 @@ impl PartialOrd for Value { Value::Binary { .. } => Some(Ordering::Greater), Value::CellPath { val: rhs, .. } => lhs.partial_cmp(rhs), Value::CustomValue { .. } => Some(Ordering::Less), + Value::MatchPattern { .. } => Some(Ordering::Less), }, (Value::CustomValue { val: lhs, .. }, rhs) => lhs.partial_cmp(rhs), (Value::LazyRecord { val, .. }, rhs) => { @@ -2061,6 +2091,27 @@ impl PartialOrd for Value { None } } + (Value::MatchPattern { .. }, rhs) => match rhs { + Value::Bool { .. } => Some(Ordering::Greater), + Value::Int { .. } => Some(Ordering::Greater), + Value::Float { .. } => Some(Ordering::Greater), + Value::Filesize { .. } => Some(Ordering::Greater), + Value::Duration { .. } => Some(Ordering::Greater), + Value::Date { .. } => Some(Ordering::Greater), + Value::Range { .. } => Some(Ordering::Greater), + Value::String { .. } => Some(Ordering::Greater), + Value::Record { .. } => Some(Ordering::Greater), + Value::LazyRecord { .. } => Some(Ordering::Greater), + Value::List { .. } => Some(Ordering::Greater), + Value::Block { .. } => Some(Ordering::Greater), + Value::Closure { .. } => Some(Ordering::Greater), + Value::Nothing { .. } => Some(Ordering::Greater), + Value::Error { .. } => Some(Ordering::Greater), + Value::Binary { .. } => Some(Ordering::Greater), + Value::CellPath { .. } => Some(Ordering::Greater), + Value::CustomValue { .. } => Some(Ordering::Greater), + Value::MatchPattern { .. } => None, + }, } } } diff --git a/crates/nu-utils/src/sample_config/default_config.nu b/crates/nu-utils/src/sample_config/default_config.nu index a97ff0c06b..ccb87a1cc8 100644 --- a/crates/nu-utils/src/sample_config/default_config.nu +++ b/crates/nu-utils/src/sample_config/default_config.nu @@ -70,6 +70,7 @@ let dark_theme = { shape_internalcall: cyan_bold shape_list: cyan_bold shape_literal: blue + shape_match_pattern: green shape_matching_brackets: { attr: u } shape_nothing: light_cyan shape_operator: yellow @@ -151,6 +152,7 @@ let light_theme = { shape_internalcall: cyan_bold shape_list: cyan_bold shape_literal: blue + shape_match_pattern: green shape_matching_brackets: { attr: u } shape_nothing: light_cyan shape_operator: yellow