From 5478bdff0ececf08c5ec6c31717a2c27a3d41b5d Mon Sep 17 00:00:00 2001 From: Bahex <17417311+Bahex@users.noreply.github.com> Date: Mon, 11 Aug 2025 23:47:31 +0300 Subject: [PATCH] feat: `null` values can be spread as if they are empty lists or records. (#16399) # Description Spread operator `...` treats `null` values as empty collections, , whichever of list or record is appropriate. # User-Facing Changes `null` values can be used with the spread operator (`...`) --- crates/nu-engine/src/eval_ir.rs | 39 ++++++++++++++++------------- crates/nu-protocol/src/ast/call.rs | 1 + crates/nu-protocol/src/eval_base.rs | 1 + crates/nu-protocol/src/ir/call.rs | 1 + tests/repl/test_spread.rs | 26 +++++++++++++++++++ 5 files changed, 50 insertions(+), 18 deletions(-) diff --git a/crates/nu-engine/src/eval_ir.rs b/crates/nu-engine/src/eval_ir.rs index 106ac03ca8..66796e2344 100644 --- a/crates/nu-engine/src/eval_ir.rs +++ b/crates/nu-engine/src/eval_ir.rs @@ -612,12 +612,13 @@ fn eval_instruction( let items = ctx.collect_reg(*items, *span)?; let list_span = list_value.span(); let items_span = items.span(); + let items = match items { + Value::List { vals, .. } => vals, + Value::Nothing { .. } => Vec::new(), + _ => return Err(ShellError::CannotSpreadAsList { span: items_span }), + }; let mut list = list_value.into_list()?; - list.extend( - items - .into_list() - .map_err(|_| ShellError::CannotSpreadAsList { span: items_span })?, - ); + list.extend(items); ctx.put_reg(*src_dst, Value::list(list, list_span).into_pipeline_data()); Ok(Continue) } @@ -649,11 +650,13 @@ fn eval_instruction( let record_span = record_value.span(); let items_span = items.span(); let mut record = record_value.into_record()?; + let items = match items { + Value::Record { val, .. } => val.into_owned(), + Value::Nothing { .. } => Record::new(), + _ => return Err(ShellError::CannotSpreadAsRecord { span: items_span }), + }; // Not using .extend() here because it doesn't handle duplicates - for (key, val) in items - .into_record() - .map_err(|_| ShellError::CannotSpreadAsRecord { span: items_span })? - { + for (key, val) in items { if let Some(first_value) = record.insert(&key, val) { return Err(ShellError::ColumnDefinedTwice { col_name: key, @@ -1191,19 +1194,19 @@ fn gather_arguments( vals, span: spread_span, .. - } => { - if let Value::List { vals, .. } = vals { + } => match vals { + Value::List { vals, .. } => { rest.extend(vals); - // Rest variable should span the spread syntax, not the list values rest_span = Some(rest_span.map_or(spread_span, |s| s.append(spread_span))); - // All further positional args should go to spread always_spread = true; - } else if let Value::Error { error, .. } = vals { - return Err(*error); - } else { - return Err(ShellError::CannotSpreadAsList { span: vals.span() }); } - } + Value::Nothing { .. } => { + rest_span = Some(rest_span.map_or(spread_span, |s| s.append(spread_span))); + always_spread = true; + } + Value::Error { error, .. } => return Err(*error), + _ => return Err(ShellError::CannotSpreadAsList { span: vals.span() }), + }, Argument::Flag { data, name, diff --git a/crates/nu-protocol/src/ast/call.rs b/crates/nu-protocol/src/ast/call.rs index 868ec53bdb..6588d6be9c 100644 --- a/crates/nu-protocol/src/ast/call.rs +++ b/crates/nu-protocol/src/ast/call.rs @@ -316,6 +316,7 @@ impl Call { if spread { match result { Value::List { mut vals, .. } => output.append(&mut vals), + Value::Nothing { .. } => (), _ => return Err(ShellError::CannotSpreadAsList { span: expr.span }), } } else { diff --git a/crates/nu-protocol/src/eval_base.rs b/crates/nu-protocol/src/eval_base.rs index e4523b2ac7..71877ff187 100644 --- a/crates/nu-protocol/src/eval_base.rs +++ b/crates/nu-protocol/src/eval_base.rs @@ -78,6 +78,7 @@ pub trait Eval { ListItem::Item(expr) => output.push(Self::eval::(state, mut_state, expr)?), ListItem::Spread(_, expr) => match Self::eval::(state, mut_state, expr)? { Value::List { vals, .. } => output.extend(vals), + Value::Nothing { .. } => (), _ => return Err(ShellError::CannotSpreadAsList { span: expr_span }), }, } diff --git a/crates/nu-protocol/src/ir/call.rs b/crates/nu-protocol/src/ir/call.rs index 6f65c4edc5..5795c09fd3 100644 --- a/crates/nu-protocol/src/ir/call.rs +++ b/crates/nu-protocol/src/ir/call.rs @@ -187,6 +187,7 @@ impl Call { if spread { match rest_val { Value::List { vals, .. } => acc.extend(vals.iter().cloned()), + Value::Nothing { .. } => (), Value::Error { error, .. } => return Err(ShellError::clone(error)), _ => { return Err(ShellError::CannotSpreadAsList { diff --git a/tests/repl/test_spread.rs b/tests/repl/test_spread.rs index 091d885f30..d4d87d0177 100644 --- a/tests/repl/test_spread.rs +++ b/tests/repl/test_spread.rs @@ -200,3 +200,29 @@ fn respect_shape() -> TestResult { run_test(r#"def "...$foo" [] {2}; do { ...$foo }"#, "2").unwrap(); run_test(r#"match "...$foo" { ...$foo => 5 }"#, "5") } + +#[test] +fn spread_null() -> TestResult { + // Spread in list + run_test(r#"[1, 2, ...(null)] | to nuon --raw"#, r#"[1,2]"#)?; + + // Spread in record + run_test(r#"{a: 1, b: 2, ...(null)} | to nuon --raw"#, r#"{a:1,b:2}"#)?; + + // Spread to built-in command's ...rest + run_test(r#"echo 1 2 ...(null) | to nuon --raw"#, r#"[1,2]"#)?; + + // Spread to custom command's ...rest + run_test( + r#" + def foo [...rest] { $rest } + foo ...(null) 1 2 ...(null) 3 | to nuon --raw + "#, + r#"[1,2,3]"#, + )?; + + // Spread to external command's arguments + assert_eq!(nu!(r#"nu --testbin cococo 1 ...(null) 2"#).out, "1 2"); + + Ok(()) +}