From 823e578c46e32e3389ff696041bfcbf3ebd73414 Mon Sep 17 00:00:00 2001 From: ysthakur <45539777+ysthakur@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:10:08 -0500 Subject: [PATCH] Spread operator for list literals (#11006) --- crates/nu-cli/src/syntax_highlight.rs | 1 + crates/nu-command/src/formats/from/nuon.rs | 6 +++ crates/nu-engine/src/eval.rs | 9 +++- crates/nu-parser/src/flatten.rs | 9 ++++ crates/nu-parser/src/parser.rs | 56 +++++++++++++++++----- crates/nu-protocol/src/ast/expr.rs | 1 + crates/nu-protocol/src/ast/expression.rs | 3 ++ crates/nu-protocol/src/eval_const.rs | 8 +++- crates/nu-protocol/src/parse_error.rs | 5 ++ crates/nu-protocol/src/shell_error.rs | 15 ++++++ src/tests.rs | 1 + src/tests/test_spread.rs | 48 +++++++++++++++++++ 12 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 src/tests/test_spread.rs diff --git a/crates/nu-cli/src/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs index 09c9dd476..8f1265499 100644 --- a/crates/nu-cli/src/syntax_highlight.rs +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -315,6 +315,7 @@ fn find_matching_block_end_in_expr( Expr::MatchBlock(_) => None, Expr::Nothing => None, Expr::Garbage => None, + Expr::Spread(_) => None, Expr::Table(hdr, rows) => { if expr_last == global_cursor_offset { diff --git a/crates/nu-command/src/formats/from/nuon.rs b/crates/nu-command/src/formats/from/nuon.rs index a8bd4fb99..8570ab900 100644 --- a/crates/nu-command/src/formats/from/nuon.rs +++ b/crates/nu-command/src/formats/from/nuon.rs @@ -348,6 +348,12 @@ fn convert_to_value( "signatures not supported in nuon".into(), expr.span, )), + Expr::Spread(..) => Err(ShellError::OutsideSpannedLabeledError( + original_text.to_string(), + "Error when loading".into(), + "spread operator not supported in nuon".into(), + expr.span, + )), Expr::String(s) => Ok(Value::string(s, span)), Expr::StringInterpolation(..) => Err(ShellError::OutsideSpannedLabeledError( original_text.to_string(), diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index 2c78fa2dc..301d35f43 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -538,7 +538,13 @@ pub fn eval_expression( Expr::List(x) => { let mut output = vec![]; for expr in x { - output.push(eval_expression(engine_state, stack, expr)?); + match &expr.expr { + Expr::Spread(expr) => match eval_expression(engine_state, stack, expr)? { + Value::List { mut vals, .. } => output.append(&mut vals), + _ => return Err(ShellError::CannotSpreadAsList { span: expr.span }), + }, + _ => output.push(eval_expression(engine_state, stack, expr)?), + } } Ok(Value::list(output, expr.span)) } @@ -635,6 +641,7 @@ pub fn eval_expression( Expr::Signature(_) => Ok(Value::nothing(expr.span)), Expr::Garbage => Ok(Value::nothing(expr.span)), Expr::Nothing => Ok(Value::nothing(expr.span)), + Expr::Spread(_) => Ok(Value::nothing(expr.span)), // Spread operator only occurs in lists } } diff --git a/crates/nu-parser/src/flatten.rs b/crates/nu-parser/src/flatten.rs index b68a8a037..6f364951a 100644 --- a/crates/nu-parser/src/flatten.rs +++ b/crates/nu-parser/src/flatten.rs @@ -500,6 +500,15 @@ pub fn flatten_expression( Expr::VarDecl(var_id) => { vec![(expr.span, FlatShape::VarDecl(*var_id))] } + + Expr::Spread(inner_expr) => { + let mut output = vec![( + Span::new(expr.span.start, expr.span.start + 3), + FlatShape::Operator, + )]; + output.extend(flatten_expression(working_set, inner_expr)); + output + } } } diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index c478729fd..47c631b68 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -3705,7 +3705,7 @@ pub fn parse_list_expression( working_set.error(err) } - let (output, err) = lite_parse(&output); + let (mut output, err) = lite_parse(&output); if let Some(err) = err { working_set.error(err) } @@ -3715,24 +3715,55 @@ pub fn parse_list_expression( let mut contained_type: Option = None; if !output.block.is_empty() { - for arg in &output.block[0].commands { + for arg in output.block.remove(0).commands { let mut spans_idx = 0; - if let LiteElement::Command(_, command) = arg { + if let LiteElement::Command(_, mut command) = arg { while spans_idx < command.parts.len() { - let arg = parse_multispan_value( - working_set, - &command.parts, - &mut spans_idx, - element_shape, - ); + let curr_span = command.parts[spans_idx]; + let curr_tok = working_set.get_span_contents(curr_span); + let (arg, ty) = if curr_tok.starts_with(b"...") + && curr_tok.len() > 3 + && (curr_tok[3] == b'$' || curr_tok[3] == b'[' || curr_tok[3] == b'(') + { + // Parse the spread operator + // Remove "..." before parsing argument to spread operator + command.parts[spans_idx] = Span::new(curr_span.start + 3, curr_span.end); + let spread_arg = parse_multispan_value( + working_set, + &command.parts, + &mut spans_idx, + &SyntaxShape::List(Box::new(element_shape.clone())), + ); + let elem_ty = match &spread_arg.ty { + Type::List(elem_ty) => *elem_ty.clone(), + _ => Type::Any, + }; + let span = Span::new(curr_span.start, spread_arg.span.end); + let spread_expr = Expression { + expr: Expr::Spread(Box::new(spread_arg)), + span, + ty: elem_ty.clone(), + custom_completion: None, + }; + (spread_expr, elem_ty) + } else { + let arg = parse_multispan_value( + working_set, + &command.parts, + &mut spans_idx, + element_shape, + ); + let ty = arg.ty.clone(); + (arg, ty) + }; if let Some(ref ctype) = contained_type { - if *ctype != arg.ty { + if *ctype != ty { contained_type = Some(Type::Any); } } else { - contained_type = Some(arg.ty.clone()); + contained_type = Some(ty); } args.push(arg); @@ -5870,6 +5901,9 @@ pub fn discover_captures_in_expr( Expr::VarDecl(var_id) => { seen.push(*var_id); } + Expr::Spread(expr) => { + discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?; + } } Ok(()) } diff --git a/crates/nu-protocol/src/ast/expr.rs b/crates/nu-protocol/src/ast/expr.rs index 4af9a1650..b10a7838b 100644 --- a/crates/nu-protocol/src/ast/expr.rs +++ b/crates/nu-protocol/src/ast/expr.rs @@ -45,6 +45,7 @@ pub enum Expr { Signature(Box), StringInterpolation(Vec), MatchPattern(Box), + Spread(Box), Nothing, Garbage, } diff --git a/crates/nu-protocol/src/ast/expression.rs b/crates/nu-protocol/src/ast/expression.rs index 1a9038164..7a6b1c1a3 100644 --- a/crates/nu-protocol/src/ast/expression.rs +++ b/crates/nu-protocol/src/ast/expression.rs @@ -289,6 +289,7 @@ impl Expression { Expr::ValueWithUnit(expr, _) => expr.has_in_variable(working_set), Expr::Var(var_id) => *var_id == IN_VARIABLE_ID, Expr::VarDecl(_) => false, + Expr::Spread(expr) => expr.has_in_variable(working_set), } } @@ -480,6 +481,7 @@ impl Expression { } } Expr::VarDecl(_) => {} + Expr::Spread(expr) => expr.replace_in_variable(working_set, new_var_id), } } @@ -618,6 +620,7 @@ impl Expression { Expr::ValueWithUnit(expr, _) => expr.replace_span(working_set, replaced, new_span), Expr::Var(_) => {} Expr::VarDecl(_) => {} + Expr::Spread(expr) => expr.replace_span(working_set, replaced, new_span), } } } diff --git a/crates/nu-protocol/src/eval_const.rs b/crates/nu-protocol/src/eval_const.rs index f062f2032..2f6518c73 100644 --- a/crates/nu-protocol/src/eval_const.rs +++ b/crates/nu-protocol/src/eval_const.rs @@ -278,7 +278,13 @@ pub fn eval_constant( Expr::List(x) => { let mut output = vec![]; for expr in x { - output.push(eval_constant(working_set, expr)?); + match &expr.expr { + Expr::Spread(expr) => match eval_constant(working_set, expr)? { + Value::List { mut vals, .. } => output.append(&mut vals), + _ => return Err(ShellError::CannotSpreadAsList { span: expr.span }), + }, + _ => output.push(eval_constant(working_set, expr)?), + } } Ok(Value::list(output, expr.span)) } diff --git a/crates/nu-protocol/src/parse_error.rs b/crates/nu-protocol/src/parse_error.rs index da763d6a9..94fd9be64 100644 --- a/crates/nu-protocol/src/parse_error.rs +++ b/crates/nu-protocol/src/parse_error.rs @@ -483,6 +483,10 @@ pub enum ParseError { #[label("Not allowed here")] Span, #[label("...and here")] Option, ), + + #[error("Unexpected spread operator outside list")] + #[diagnostic(code(nu::parser::unexpected_spread_operator))] + UnexpectedSpread(#[label("Spread operator not allowed here")] Span), } impl ParseError { @@ -569,6 +573,7 @@ impl ParseError { ParseError::InvalidLiteral(_, _, s) => *s, ParseError::LabeledErrorWithHelp { span: s, .. } => *s, ParseError::RedirectionInLetMut(s, _) => *s, + ParseError::UnexpectedSpread(s) => *s, } } } diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 7712e0a0e..df7cf4372 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -1170,6 +1170,21 @@ This is an internal Nushell error, please file an issue https://github.com/nushe )] //todo: add error detail ErrorExpandingGlob(String, #[label = "{0}"] Span), + + /// Tried spreading a non-list inside a list. + /// + /// ## Resolution + /// + /// Only lists can be spread inside lists. Try converting the value to a list before spreading. + #[error("Not a list")] + #[diagnostic( + code(nu::shell::cannot_spread), + help("Only lists can be spread inside lists. Try converting the value to a list before spreading") + )] + CannotSpreadAsList { + #[label = "cannot spread value"] + span: Span, + }, } // TODO: Implement as From trait diff --git a/src/tests.rs b/src/tests.rs index cd8c2c456..ac7c3fa4f 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -20,6 +20,7 @@ mod test_parser; mod test_ranges; mod test_regex; mod test_signatures; +mod test_spread; mod test_stdlib; mod test_strings; mod test_table_operations; diff --git a/src/tests/test_spread.rs b/src/tests/test_spread.rs new file mode 100644 index 000000000..891870eb3 --- /dev/null +++ b/src/tests/test_spread.rs @@ -0,0 +1,48 @@ +use crate::tests::{fail_test, run_test, TestResult}; + +#[test] +fn spread_in_list() -> TestResult { + run_test(r#"[...[]] | to nuon"#, "[]").unwrap(); + run_test( + r#"[1 2 ...[[3] {x: 1}] 5] | to nuon"#, + "[1, 2, [3], {x: 1}, 5]", + ) + .unwrap(); + run_test( + r#"[...("foo" | split chars) 10] | to nuon"#, + "[f, o, o, 10]", + ) + .unwrap(); + run_test( + r#"let l = [1, 2, [3]]; [...$l $l] | to nuon"#, + "[1, 2, [3], [1, 2, [3]]]", + ) + .unwrap(); + run_test( + r#"[ ...[ ...[ ...[ a ] b ] c ] d ] | to nuon"#, + "[a, b, c, d]", + ) +} + +#[test] +fn not_spread() -> TestResult { + run_test(r#"def ... [x] { $x }; ... ..."#, "...").unwrap(); + run_test( + r#"let a = 4; [... $a ... [1] ... (5) ...bare ...] | to nuon"#, + "[..., 4, ..., [1], ..., 5, ...bare, ...]", + ) +} + +#[test] +fn bad_spread_on_non_list() -> TestResult { + fail_test(r#"let x = 5; [...$x]"#, "cannot spread").unwrap(); + fail_test(r#"[...({ x: 1 })]"#, "cannot spread") +} + +#[test] +fn spread_type() -> TestResult { + run_test(r#"[1 ...[]] | describe"#, "list").unwrap(); + run_test(r#"[1 ...[2]] | describe"#, "list").unwrap(); + run_test(r#"["foo" ...[4 5 6]] | describe"#, "list").unwrap(); + run_test(r#"[1 2 ...["misfit"] 4] | describe"#, "list") +}