Skip to content

Commit

Permalink
Spread operator in record literals (nushell#11144)
Browse files Browse the repository at this point in the history
Goes towards implementing nushell#10598, which asks for a spread operator in
lists, in records, and when calling commands (continuation of nushell#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::shell::column_defined_twice

  × Record field or table column used twice: x
   ╭─[entry nushell#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
  • Loading branch information
ysthakur authored and dmatos2012 committed Feb 20, 2024
1 parent 3dcbe83 commit 2cde0a0
Show file tree
Hide file tree
Showing 11 changed files with 408 additions and 111 deletions.
15 changes: 11 additions & 4 deletions crates/nu-cli/src/syntax_highlight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use log::trace;
use nu_ansi_term::Style;
use nu_color_config::{get_matching_brackets_style, get_shape_color};
use nu_parser::{flatten_block, parse, FlatShape};
use nu_protocol::ast::{Argument, Block, Expr, Expression, PipelineElement};
use nu_protocol::ast::{Argument, Block, Expr, Expression, PipelineElement, RecordItem};
use nu_protocol::engine::{EngineState, StateWorkingSet};
use nu_protocol::{Config, Span};
use reedline::{Highlighter, StyledText};
Expand Down Expand Up @@ -365,9 +365,16 @@ fn find_matching_block_end_in_expr(
Some(expr_last)
} else {
// cursor is inside record
for (k, v) in exprs {
find_in_expr_or_continue!(k);
find_in_expr_or_continue!(v);
for expr in exprs {
match expr {
RecordItem::Pair(k, v) => {
find_in_expr_or_continue!(k);
find_in_expr_or_continue!(v);
}
RecordItem::Spread(_, record) => {
find_in_expr_or_continue!(record);
}
}
}
None
}
Expand Down
39 changes: 26 additions & 13 deletions crates/nu-command/src/formats/from/nuon.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use nu_protocol::ast::{Call, Expr, Expression, PipelineElement};
use nu_protocol::ast::{Call, Expr, Expression, PipelineElement, RecordItem};
use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet};
use nu_protocol::{
record, Category, Example, IntoPipelineData, PipelineData, Range, Record, ShellError,
Expand Down Expand Up @@ -316,22 +316,34 @@ fn convert_to_value(
Expr::Record(key_vals) => {
let mut record = Record::new();

for (key, val) in key_vals {
let key_str = match key.expr {
Expr::String(key_str) => key_str,
_ => {
for key_val in key_vals {
match key_val {
RecordItem::Pair(key, val) => {
let key_str = match key.expr {
Expr::String(key_str) => key_str,
_ => {
return Err(ShellError::OutsideSpannedLabeledError(
original_text.to_string(),
"Error when loading".into(),
"only strings can be keys".into(),
key.span,
))
}
};

let value = convert_to_value(val, span, original_text)?;

record.push(key_str, value);
}
RecordItem::Spread(_, inner) => {
return Err(ShellError::OutsideSpannedLabeledError(
original_text.to_string(),
"Error when loading".into(),
"only strings can be keys".into(),
key.span,
))
"spread operator not supported in nuon".into(),
inner.span,
));
}
};

let value = convert_to_value(val, span, original_text)?;

record.push(key_str, value);
}
}

Ok(Value::record(record, span))
Expand Down Expand Up @@ -387,6 +399,7 @@ fn convert_to_value(

if let Some(idx) = cols.iter().position(|existing| existing == key_str) {
return Err(ShellError::ColumnDefinedTwice {
col_name: key_str.clone(),
second_use: key.span,
first_use: headers[idx].span,
});
Expand Down
6 changes: 3 additions & 3 deletions crates/nu-engine/src/documentation.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use nu_protocol::ast::{Argument, Expr, Expression};
use nu_protocol::ast::{Argument, Expr, Expression, RecordItem};
use nu_protocol::{
ast::Call,
engine::{EngineState, Stack},
Expand Down Expand Up @@ -378,10 +378,10 @@ fn get_argument_for_color_value(
) -> Option<Argument> {
match color {
Value::Record { val, .. } => {
let record_exp: Vec<(Expression, Expression)> = val
let record_exp: Vec<RecordItem> = val
.into_iter()
.map(|(k, v)| {
(
RecordItem::Pair(
Expression {
expr: Expr::String(k.clone()),
span,
Expand Down
52 changes: 38 additions & 14 deletions crates/nu-engine/src/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use nu_path::expand_path_with;
use nu_protocol::{
ast::{
eval_operator, Argument, Assignment, Bits, Block, Boolean, Call, Comparison, Expr,
Expression, Math, Operator, PathMember, PipelineElement, Redirection,
Expression, Math, Operator, PathMember, PipelineElement, RecordItem, Redirection,
},
engine::{Closure, EngineState, Stack},
DeclId, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, Range, Record,
Expand Down Expand Up @@ -549,22 +549,45 @@ pub fn eval_expression(
}
Ok(Value::list(output, expr.span))
}
Expr::Record(fields) => {
Expr::Record(items) => {
let mut record = Record::new();

for (col, val) in fields {
// avoid duplicate cols.
let col_name = eval_expression(engine_state, stack, col)?.as_string()?;
let pos = record.index_of(&col_name);
match pos {
Some(index) => {
return Err(ShellError::ColumnDefinedTwice {
second_use: col.span,
first_use: fields[index].0.span,
})
let mut col_names = HashMap::new();

for item in items {
match item {
RecordItem::Pair(col, val) => {
// avoid duplicate cols
let col_name = eval_expression(engine_state, stack, col)?.as_string()?;
if let Some(orig_span) = col_names.get(&col_name) {
return Err(ShellError::ColumnDefinedTwice {
col_name,
second_use: col.span,
first_use: *orig_span,
});
} else {
col_names.insert(col_name.clone(), col.span);
record.push(col_name, eval_expression(engine_state, stack, val)?);
}
}
None => {
record.push(col_name, eval_expression(engine_state, stack, val)?);
RecordItem::Spread(_, inner) => {
match eval_expression(engine_state, stack, inner)? {
Value::Record { val: inner_val, .. } => {
for (col_name, val) in inner_val {
if let Some(orig_span) = col_names.get(&col_name) {
return Err(ShellError::ColumnDefinedTwice {
col_name,
second_use: inner.span,
first_use: *orig_span,
});
} else {
col_names.insert(col_name.clone(), inner.span);
record.push(col_name, val);
}
}
}
_ => return Err(ShellError::CannotSpreadAsRecord { span: inner.span }),
}
}
}
}
Expand All @@ -580,6 +603,7 @@ pub fn eval_expression(
.position(|existing| existing == &header)
{
return Err(ShellError::ColumnDefinedTwice {
col_name: header,
second_use: expr.span,
first_use: headers[idx].span,
});
Expand Down
56 changes: 37 additions & 19 deletions crates/nu-parser/src/flatten.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use nu_protocol::ast::{
Block, Expr, Expression, ImportPatternMember, MatchPattern, PathMember, Pattern, Pipeline,
PipelineElement,
PipelineElement, RecordItem,
};
use nu_protocol::{engine::StateWorkingSet, Span};
use nu_protocol::{DeclId, VarId};
Expand Down Expand Up @@ -410,29 +410,47 @@ pub fn flatten_expression(

let mut output = vec![];
for l in list {
let flattened_lhs = flatten_expression(working_set, &l.0);
let flattened_rhs = flatten_expression(working_set, &l.1);
match l {
RecordItem::Pair(key, val) => {
let flattened_lhs = flatten_expression(working_set, key);
let flattened_rhs = flatten_expression(working_set, val);

if let Some(first) = flattened_lhs.first() {
if first.0.start > last_end {
output
.push((Span::new(last_end, first.0.start), FlatShape::Record));
}
}
if let Some(last) = flattened_lhs.last() {
last_end = last.0.end;
}
output.extend(flattened_lhs);

if let Some(first) = flattened_lhs.first() {
if first.0.start > last_end {
output.push((Span::new(last_end, first.0.start), FlatShape::Record));
if let Some(first) = flattened_rhs.first() {
if first.0.start > last_end {
output
.push((Span::new(last_end, first.0.start), FlatShape::Record));
}
}
if let Some(last) = flattened_rhs.last() {
last_end = last.0.end;
}

output.extend(flattened_rhs);
}
}
if let Some(last) = flattened_lhs.last() {
last_end = last.0.end;
}
output.extend(flattened_lhs);
RecordItem::Spread(op_span, record) => {
if op_span.start > last_end {
output.push((Span::new(last_end, op_span.start), FlatShape::Record));
}
output.push((*op_span, FlatShape::Operator));

if let Some(first) = flattened_rhs.first() {
if first.0.start > last_end {
output.push((Span::new(last_end, first.0.start), FlatShape::Record));
let flattened_inner = flatten_expression(working_set, record);
if let Some(last) = flattened_inner.last() {
last_end = last.0.end;
}
output.extend(flattened_inner);
}
}
if let Some(last) = flattened_rhs.last() {
last_end = last.0.end;
}

output.extend(flattened_rhs);
}
if last_end < outer_span.end {
output.push((Span::new(last_end, outer_span.end), FlatShape::Record));
Expand Down
Loading

0 comments on commit 2cde0a0

Please sign in to comment.