mirror of
https://github.com/nushell/nushell.git
synced 2025-06-30 22:50:14 +02:00
Spread operator in record literals (#11144)
Goes towards implementing #10598, which asks for a spread operator in lists, in records, and when calling commands (continuation of #11006, which only implements it in lists) # Description This PR is for adding a spread operator that can be used when building records. Additional functionality can be added later. Changes: - Previously, the `Expr::Record` variant held `(Expression, Expression)` pairs. It now holds instances of an enum `RecordItem` (the name isn't amazing) that allows either a key-value mapping or a spread operator. - `...` will be treated as the spread operator when it appears before `$`, `{`, or `(` inside records (no whitespace allowed in between) (not implemented yet) - The error message for duplicate columns now includes the column name itself, because if two spread records are involved in such an error, you can't tell which field was duplicated from the spans alone `...` will still be treated as a normal string outside records, and even in records, it is not treated as a spread operator when not followed immediately by a `$`, `{`, or `(`. # User-Facing Changes Users will be able to use `...` when building records. ``` > let rec = { x: 1, ...{ a: 2 } } > $rec ╭───┬───╮ │ x │ 1 │ │ a │ 2 │ ╰───┴───╯ > { foo: bar, ...$rec, baz: blah } ╭─────┬──────╮ │ foo │ bar │ │ x │ 1 │ │ a │ 2 │ │ baz │ blah │ ╰─────┴──────╯ ``` If you want to update a field of a record, you'll have to use `merge` instead: ``` > { ...$rec, x: 5 } Error: nu:🐚:column_defined_twice × Record field or table column used twice: x ╭─[entry #2:1:1] 1 │ { ...$rec, x: 5 } · ──┬─ ┬ · │ ╰── field redefined here · ╰── field first defined here ╰──── > $rec | merge { x: 5 } ╭───┬───╮ │ x │ 5 │ │ a │ 2 │ ╰───┴───╯ ``` # Tests + Formatting # After Submitting
This commit is contained in:
@ -14,6 +14,7 @@ use nu_protocol::{
|
||||
Argument, Assignment, Bits, Block, Boolean, Call, CellPath, Comparison, Expr, Expression,
|
||||
FullCellPath, ImportPattern, ImportPatternHead, ImportPatternMember, MatchPattern, Math,
|
||||
Operator, PathMember, Pattern, Pipeline, PipelineElement, RangeInclusion, RangeOperator,
|
||||
RecordItem,
|
||||
},
|
||||
engine::StateWorkingSet,
|
||||
eval_const::{eval_constant, value_as_string},
|
||||
@ -1619,6 +1620,10 @@ pub fn parse_brace_expr(
|
||||
parse_closure_expression(working_set, shape, span)
|
||||
} else if matches!(third_token, Some(b":")) {
|
||||
parse_full_cell_path(working_set, None, span)
|
||||
} else if second_token.is_some_and(|c| {
|
||||
c.len() > 3 && c.starts_with(b"...") && (c[3] == b'$' || c[3] == b'{' || c[3] == b'(')
|
||||
}) {
|
||||
parse_record(working_set, span)
|
||||
} else if matches!(shape, SyntaxShape::Closure(_)) || matches!(shape, SyntaxShape::Any) {
|
||||
parse_closure_expression(working_set, shape, span)
|
||||
} else if matches!(shape, SyntaxShape::Block) {
|
||||
@ -5199,33 +5204,68 @@ pub fn parse_record(working_set: &mut StateWorkingSet, span: Span) -> Expression
|
||||
|
||||
let mut field_types = Some(vec![]);
|
||||
while idx < tokens.len() {
|
||||
let field = parse_value(working_set, tokens[idx].span, &SyntaxShape::Any);
|
||||
let curr_span = tokens[idx].span;
|
||||
let curr_tok = working_set.get_span_contents(curr_span);
|
||||
if curr_tok.starts_with(b"...")
|
||||
&& curr_tok.len() > 3
|
||||
&& (curr_tok[3] == b'$' || curr_tok[3] == b'{' || curr_tok[3] == b'(')
|
||||
{
|
||||
// Parse spread operator
|
||||
let inner = parse_value(
|
||||
working_set,
|
||||
Span::new(curr_span.start + 3, curr_span.end),
|
||||
&SyntaxShape::Record(vec![]),
|
||||
);
|
||||
idx += 1;
|
||||
|
||||
idx += 1;
|
||||
if idx == tokens.len() {
|
||||
working_set.error(ParseError::Expected("record", span));
|
||||
return garbage(span);
|
||||
}
|
||||
let colon = working_set.get_span_contents(tokens[idx].span);
|
||||
idx += 1;
|
||||
if idx == tokens.len() || colon != b":" {
|
||||
//FIXME: need better error
|
||||
working_set.error(ParseError::Expected("record", span));
|
||||
return garbage(span);
|
||||
}
|
||||
let value = parse_value(working_set, tokens[idx].span, &SyntaxShape::Any);
|
||||
idx += 1;
|
||||
|
||||
if let Some(field) = field.as_string() {
|
||||
if let Some(fields) = &mut field_types {
|
||||
fields.push((field, value.ty.clone()));
|
||||
match &inner.ty {
|
||||
Type::Record(inner_fields) => {
|
||||
if let Some(fields) = &mut field_types {
|
||||
for (field, ty) in inner_fields {
|
||||
fields.push((field.clone(), ty.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// We can't properly see all the field types
|
||||
// so fall back to the Any type later
|
||||
field_types = None;
|
||||
}
|
||||
}
|
||||
output.push(RecordItem::Spread(
|
||||
Span::new(curr_span.start, curr_span.start + 3),
|
||||
inner,
|
||||
));
|
||||
} else {
|
||||
// We can't properly see all the field types
|
||||
// so fall back to the Any type later
|
||||
field_types = None;
|
||||
// Normal key-value pair
|
||||
let field = parse_value(working_set, curr_span, &SyntaxShape::Any);
|
||||
|
||||
idx += 1;
|
||||
if idx == tokens.len() {
|
||||
working_set.error(ParseError::Expected("record", span));
|
||||
return garbage(span);
|
||||
}
|
||||
let colon = working_set.get_span_contents(tokens[idx].span);
|
||||
idx += 1;
|
||||
if idx == tokens.len() || colon != b":" {
|
||||
//FIXME: need better error
|
||||
working_set.error(ParseError::Expected("record", span));
|
||||
return garbage(span);
|
||||
}
|
||||
let value = parse_value(working_set, tokens[idx].span, &SyntaxShape::Any);
|
||||
idx += 1;
|
||||
|
||||
if let Some(field) = field.as_string() {
|
||||
if let Some(fields) = &mut field_types {
|
||||
fields.push((field, value.ty.clone()));
|
||||
}
|
||||
} else {
|
||||
// We can't properly see all the field types
|
||||
// so fall back to the Any type later
|
||||
field_types = None;
|
||||
}
|
||||
output.push(RecordItem::Pair(field, value));
|
||||
}
|
||||
output.push((field, value));
|
||||
}
|
||||
|
||||
Expression {
|
||||
@ -5838,10 +5878,29 @@ pub fn discover_captures_in_expr(
|
||||
discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?;
|
||||
}
|
||||
}
|
||||
Expr::Record(fields) => {
|
||||
for (field_name, field_value) in fields {
|
||||
discover_captures_in_expr(working_set, field_name, seen, seen_blocks, output)?;
|
||||
discover_captures_in_expr(working_set, field_value, seen, seen_blocks, output)?;
|
||||
Expr::Record(items) => {
|
||||
for item in items {
|
||||
match item {
|
||||
RecordItem::Pair(field_name, field_value) => {
|
||||
discover_captures_in_expr(
|
||||
working_set,
|
||||
field_name,
|
||||
seen,
|
||||
seen_blocks,
|
||||
output,
|
||||
)?;
|
||||
discover_captures_in_expr(
|
||||
working_set,
|
||||
field_value,
|
||||
seen,
|
||||
seen_blocks,
|
||||
output,
|
||||
)?;
|
||||
}
|
||||
RecordItem::Spread(_, record) => {
|
||||
discover_captures_in_expr(working_set, record, seen, seen_blocks, output)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::Signature(sig) => {
|
||||
|
Reference in New Issue
Block a user