mirror of
https://github.com/nushell/nushell
synced 2024-12-26 13:03:07 +00: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:
parent
0e1322e6d6
commit
0303d709e6
11 changed files with 410 additions and 113 deletions
|
@ -2,7 +2,7 @@ use log::trace;
|
||||||
use nu_ansi_term::Style;
|
use nu_ansi_term::Style;
|
||||||
use nu_color_config::{get_matching_brackets_style, get_shape_color};
|
use nu_color_config::{get_matching_brackets_style, get_shape_color};
|
||||||
use nu_parser::{flatten_block, parse, FlatShape};
|
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::engine::{EngineState, StateWorkingSet};
|
||||||
use nu_protocol::{Config, Span};
|
use nu_protocol::{Config, Span};
|
||||||
use reedline::{Highlighter, StyledText};
|
use reedline::{Highlighter, StyledText};
|
||||||
|
@ -365,9 +365,16 @@ fn find_matching_block_end_in_expr(
|
||||||
Some(expr_last)
|
Some(expr_last)
|
||||||
} else {
|
} else {
|
||||||
// cursor is inside record
|
// cursor is inside record
|
||||||
for (k, v) in exprs {
|
for expr in exprs {
|
||||||
find_in_expr_or_continue!(k);
|
match expr {
|
||||||
find_in_expr_or_continue!(v);
|
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
|
None
|
||||||
}
|
}
|
||||||
|
|
|
@ -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::engine::{Command, EngineState, Stack, StateWorkingSet};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
record, Category, Example, IntoPipelineData, PipelineData, Range, Record, ShellError,
|
record, Category, Example, IntoPipelineData, PipelineData, Range, Record, ShellError,
|
||||||
|
@ -316,22 +316,34 @@ fn convert_to_value(
|
||||||
Expr::Record(key_vals) => {
|
Expr::Record(key_vals) => {
|
||||||
let mut record = Record::new();
|
let mut record = Record::new();
|
||||||
|
|
||||||
for (key, val) in key_vals {
|
for key_val in key_vals {
|
||||||
let key_str = match key.expr {
|
match key_val {
|
||||||
Expr::String(key_str) => key_str,
|
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(
|
return Err(ShellError::OutsideSpannedLabeledError(
|
||||||
original_text.to_string(),
|
original_text.to_string(),
|
||||||
"Error when loading".into(),
|
"Error when loading".into(),
|
||||||
"only strings can be keys".into(),
|
"spread operator not supported in nuon".into(),
|
||||||
key.span,
|
inner.span,
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
let value = convert_to_value(val, span, original_text)?;
|
|
||||||
|
|
||||||
record.push(key_str, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Value::record(record, span))
|
Ok(Value::record(record, span))
|
||||||
|
@ -387,6 +399,7 @@ fn convert_to_value(
|
||||||
|
|
||||||
if let Some(idx) = cols.iter().position(|existing| existing == key_str) {
|
if let Some(idx) = cols.iter().position(|existing| existing == key_str) {
|
||||||
return Err(ShellError::ColumnDefinedTwice {
|
return Err(ShellError::ColumnDefinedTwice {
|
||||||
|
col_name: key_str.clone(),
|
||||||
second_use: key.span,
|
second_use: key.span,
|
||||||
first_use: headers[idx].span,
|
first_use: headers[idx].span,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use nu_protocol::ast::{Argument, Expr, Expression};
|
use nu_protocol::ast::{Argument, Expr, Expression, RecordItem};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::Call,
|
ast::Call,
|
||||||
engine::{EngineState, Stack},
|
engine::{EngineState, Stack},
|
||||||
|
@ -378,10 +378,10 @@ fn get_argument_for_color_value(
|
||||||
) -> Option<Argument> {
|
) -> Option<Argument> {
|
||||||
match color {
|
match color {
|
||||||
Value::Record { val, .. } => {
|
Value::Record { val, .. } => {
|
||||||
let record_exp: Vec<(Expression, Expression)> = val
|
let record_exp: Vec<RecordItem> = val
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(k, v)| {
|
.map(|(k, v)| {
|
||||||
(
|
RecordItem::Pair(
|
||||||
Expression {
|
Expression {
|
||||||
expr: Expr::String(k.clone()),
|
expr: Expr::String(k.clone()),
|
||||||
span,
|
span,
|
||||||
|
|
|
@ -3,7 +3,7 @@ use nu_path::expand_path_with;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::{
|
ast::{
|
||||||
eval_operator, Argument, Assignment, Bits, Block, Boolean, Call, Comparison, Expr,
|
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},
|
engine::{Closure, EngineState, Stack},
|
||||||
DeclId, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, Range, Record,
|
DeclId, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, Range, Record,
|
||||||
|
@ -549,22 +549,45 @@ pub fn eval_expression(
|
||||||
}
|
}
|
||||||
Ok(Value::list(output, expr.span))
|
Ok(Value::list(output, expr.span))
|
||||||
}
|
}
|
||||||
Expr::Record(fields) => {
|
Expr::Record(items) => {
|
||||||
let mut record = Record::new();
|
let mut record = Record::new();
|
||||||
|
|
||||||
for (col, val) in fields {
|
let mut col_names = HashMap::new();
|
||||||
// avoid duplicate cols.
|
|
||||||
let col_name = eval_expression(engine_state, stack, col)?.as_string()?;
|
for item in items {
|
||||||
let pos = record.index_of(&col_name);
|
match item {
|
||||||
match pos {
|
RecordItem::Pair(col, val) => {
|
||||||
Some(index) => {
|
// avoid duplicate cols
|
||||||
return Err(ShellError::ColumnDefinedTwice {
|
let col_name = eval_expression(engine_state, stack, col)?.as_string()?;
|
||||||
second_use: col.span,
|
if let Some(orig_span) = col_names.get(&col_name) {
|
||||||
first_use: fields[index].0.span,
|
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 => {
|
RecordItem::Spread(_, inner) => {
|
||||||
record.push(col_name, eval_expression(engine_state, stack, val)?);
|
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 }),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -580,6 +603,7 @@ pub fn eval_expression(
|
||||||
.position(|existing| existing == &header)
|
.position(|existing| existing == &header)
|
||||||
{
|
{
|
||||||
return Err(ShellError::ColumnDefinedTwice {
|
return Err(ShellError::ColumnDefinedTwice {
|
||||||
|
col_name: header,
|
||||||
second_use: expr.span,
|
second_use: expr.span,
|
||||||
first_use: headers[idx].span,
|
first_use: headers[idx].span,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use nu_protocol::ast::{
|
use nu_protocol::ast::{
|
||||||
Block, Expr, Expression, ImportPatternMember, MatchPattern, PathMember, Pattern, Pipeline,
|
Block, Expr, Expression, ImportPatternMember, MatchPattern, PathMember, Pattern, Pipeline,
|
||||||
PipelineElement,
|
PipelineElement, RecordItem,
|
||||||
};
|
};
|
||||||
use nu_protocol::{engine::StateWorkingSet, Span};
|
use nu_protocol::{engine::StateWorkingSet, Span};
|
||||||
use nu_protocol::{DeclId, VarId};
|
use nu_protocol::{DeclId, VarId};
|
||||||
|
@ -410,29 +410,47 @@ pub fn flatten_expression(
|
||||||
|
|
||||||
let mut output = vec![];
|
let mut output = vec![];
|
||||||
for l in list {
|
for l in list {
|
||||||
let flattened_lhs = flatten_expression(working_set, &l.0);
|
match l {
|
||||||
let flattened_rhs = flatten_expression(working_set, &l.1);
|
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 let Some(first) = flattened_lhs.first() {
|
||||||
if first.0.start > last_end {
|
if first.0.start > last_end {
|
||||||
output.push((Span::new(last_end, first.0.start), FlatShape::Record));
|
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_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);
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
|
||||||
|
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_lhs.last() {
|
|
||||||
last_end = last.0.end;
|
|
||||||
}
|
|
||||||
output.extend(flattened_lhs);
|
|
||||||
|
|
||||||
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 last_end < outer_span.end {
|
if last_end < outer_span.end {
|
||||||
output.push((Span::new(last_end, outer_span.end), FlatShape::Record));
|
output.push((Span::new(last_end, outer_span.end), FlatShape::Record));
|
||||||
|
|
|
@ -14,6 +14,7 @@ use nu_protocol::{
|
||||||
Argument, Assignment, Bits, Block, Boolean, Call, CellPath, Comparison, Expr, Expression,
|
Argument, Assignment, Bits, Block, Boolean, Call, CellPath, Comparison, Expr, Expression,
|
||||||
FullCellPath, ImportPattern, ImportPatternHead, ImportPatternMember, MatchPattern, Math,
|
FullCellPath, ImportPattern, ImportPatternHead, ImportPatternMember, MatchPattern, Math,
|
||||||
Operator, PathMember, Pattern, Pipeline, PipelineElement, RangeInclusion, RangeOperator,
|
Operator, PathMember, Pattern, Pipeline, PipelineElement, RangeInclusion, RangeOperator,
|
||||||
|
RecordItem,
|
||||||
},
|
},
|
||||||
engine::StateWorkingSet,
|
engine::StateWorkingSet,
|
||||||
eval_const::{eval_constant, value_as_string},
|
eval_const::{eval_constant, value_as_string},
|
||||||
|
@ -1619,6 +1620,10 @@ pub fn parse_brace_expr(
|
||||||
parse_closure_expression(working_set, shape, span)
|
parse_closure_expression(working_set, shape, span)
|
||||||
} else if matches!(third_token, Some(b":")) {
|
} else if matches!(third_token, Some(b":")) {
|
||||||
parse_full_cell_path(working_set, None, span)
|
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) {
|
} else if matches!(shape, SyntaxShape::Closure(_)) || matches!(shape, SyntaxShape::Any) {
|
||||||
parse_closure_expression(working_set, shape, span)
|
parse_closure_expression(working_set, shape, span)
|
||||||
} else if matches!(shape, SyntaxShape::Block) {
|
} 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![]);
|
let mut field_types = Some(vec![]);
|
||||||
while idx < tokens.len() {
|
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;
|
match &inner.ty {
|
||||||
if idx == tokens.len() {
|
Type::Record(inner_fields) => {
|
||||||
working_set.error(ParseError::Expected("record", span));
|
if let Some(fields) = &mut field_types {
|
||||||
return garbage(span);
|
for (field, ty) in inner_fields {
|
||||||
}
|
fields.push((field.clone(), ty.clone()));
|
||||||
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));
|
// We can't properly see all the field types
|
||||||
return garbage(span);
|
// so fall back to the Any type later
|
||||||
}
|
field_types = None;
|
||||||
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()));
|
|
||||||
}
|
}
|
||||||
|
output.push(RecordItem::Spread(
|
||||||
|
Span::new(curr_span.start, curr_span.start + 3),
|
||||||
|
inner,
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
// We can't properly see all the field types
|
// Normal key-value pair
|
||||||
// so fall back to the Any type later
|
let field = parse_value(working_set, curr_span, &SyntaxShape::Any);
|
||||||
field_types = None;
|
|
||||||
|
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 {
|
Expression {
|
||||||
|
@ -5838,10 +5878,29 @@ pub fn discover_captures_in_expr(
|
||||||
discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?;
|
discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::Record(fields) => {
|
Expr::Record(items) => {
|
||||||
for (field_name, field_value) in fields {
|
for item in items {
|
||||||
discover_captures_in_expr(working_set, field_name, seen, seen_blocks, output)?;
|
match item {
|
||||||
discover_captures_in_expr(working_set, field_value, seen, seen_blocks, output)?;
|
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) => {
|
Expr::Signature(sig) => {
|
||||||
|
|
|
@ -30,7 +30,7 @@ pub enum Expr {
|
||||||
MatchBlock(Vec<(MatchPattern, Expression)>),
|
MatchBlock(Vec<(MatchPattern, Expression)>),
|
||||||
List(Vec<Expression>),
|
List(Vec<Expression>),
|
||||||
Table(Vec<Expression>, Vec<Vec<Expression>>),
|
Table(Vec<Expression>, Vec<Vec<Expression>>),
|
||||||
Record(Vec<(Expression, Expression)>),
|
Record(Vec<RecordItem>),
|
||||||
Keyword(Vec<u8>, Span, Box<Expression>),
|
Keyword(Vec<u8>, Span, Box<Expression>),
|
||||||
ValueWithUnit(Box<Expression>, Spanned<Unit>),
|
ValueWithUnit(Box<Expression>, Spanned<Unit>),
|
||||||
DateTime(chrono::DateTime<FixedOffset>),
|
DateTime(chrono::DateTime<FixedOffset>),
|
||||||
|
@ -49,3 +49,11 @@ pub enum Expr {
|
||||||
Nothing,
|
Nothing,
|
||||||
Garbage,
|
Garbage,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum RecordItem {
|
||||||
|
/// A key: val mapping
|
||||||
|
Pair(Expression, Expression),
|
||||||
|
/// Span for the "..." and the expression that's being spread
|
||||||
|
Spread(Span, Expression),
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::Expr;
|
use super::{Expr, RecordItem};
|
||||||
use crate::ast::ImportPattern;
|
use crate::ast::ImportPattern;
|
||||||
use crate::DeclId;
|
use crate::DeclId;
|
||||||
use crate::{engine::StateWorkingSet, BlockId, Signature, Span, Type, VarId, IN_VARIABLE_ID};
|
use crate::{engine::StateWorkingSet, BlockId, Signature, Span, Type, VarId, IN_VARIABLE_ID};
|
||||||
|
@ -242,13 +242,22 @@ impl Expression {
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
Expr::Record(fields) => {
|
Expr::Record(items) => {
|
||||||
for (field_name, field_value) in fields {
|
for item in items {
|
||||||
if field_name.has_in_variable(working_set) {
|
match item {
|
||||||
return true;
|
RecordItem::Pair(field_name, field_value) => {
|
||||||
}
|
if field_name.has_in_variable(working_set) {
|
||||||
if field_value.has_in_variable(working_set) {
|
return true;
|
||||||
return true;
|
}
|
||||||
|
if field_value.has_in_variable(working_set) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RecordItem::Spread(_, record) => {
|
||||||
|
if record.has_in_variable(working_set) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
|
@ -418,10 +427,17 @@ impl Expression {
|
||||||
right.replace_in_variable(working_set, new_var_id)
|
right.replace_in_variable(working_set, new_var_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::Record(fields) => {
|
Expr::Record(items) => {
|
||||||
for (field_name, field_value) in fields {
|
for item in items {
|
||||||
field_name.replace_in_variable(working_set, new_var_id);
|
match item {
|
||||||
field_value.replace_in_variable(working_set, new_var_id);
|
RecordItem::Pair(field_name, field_value) => {
|
||||||
|
field_name.replace_in_variable(working_set, new_var_id);
|
||||||
|
field_value.replace_in_variable(working_set, new_var_id);
|
||||||
|
}
|
||||||
|
RecordItem::Spread(_, record) => {
|
||||||
|
record.replace_in_variable(working_set, new_var_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::Signature(_) => {}
|
Expr::Signature(_) => {}
|
||||||
|
@ -581,10 +597,17 @@ impl Expression {
|
||||||
right.replace_span(working_set, replaced, new_span)
|
right.replace_span(working_set, replaced, new_span)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::Record(fields) => {
|
Expr::Record(items) => {
|
||||||
for (field_name, field_value) in fields {
|
for item in items {
|
||||||
field_name.replace_span(working_set, replaced, new_span);
|
match item {
|
||||||
field_value.replace_span(working_set, replaced, new_span);
|
RecordItem::Pair(field_name, field_value) => {
|
||||||
|
field_name.replace_span(working_set, replaced, new_span);
|
||||||
|
field_value.replace_span(working_set, replaced, new_span);
|
||||||
|
}
|
||||||
|
RecordItem::Spread(_, record) => {
|
||||||
|
record.replace_span(working_set, replaced, new_span);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::Signature(_) => {}
|
Expr::Signature(_) => {}
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::{
|
ast::{
|
||||||
eval_operator, Bits, Block, Boolean, Call, Comparison, Expr, Expression, Math, Operator,
|
eval_operator, Bits, Block, Boolean, Call, Comparison, Expr, Expression, Math, Operator,
|
||||||
PipelineElement,
|
PipelineElement, RecordItem,
|
||||||
},
|
},
|
||||||
engine::{EngineState, StateWorkingSet},
|
engine::{EngineState, StateWorkingSet},
|
||||||
record, HistoryFileFormat, PipelineData, Range, Record, ShellError, Span, Value,
|
record, HistoryFileFormat, PipelineData, Range, Record, ShellError, Span, Value,
|
||||||
};
|
};
|
||||||
use nu_system::os_info::{get_kernel_version, get_os_arch, get_os_family, get_os_name};
|
use nu_system::os_info::{get_kernel_version, get_os_arch, get_os_family, get_os_name};
|
||||||
use std::path::{Path, PathBuf};
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
pub fn create_nu_constant(engine_state: &EngineState, span: Span) -> Result<Value, ShellError> {
|
pub fn create_nu_constant(engine_state: &EngineState, span: Span) -> Result<Value, ShellError> {
|
||||||
fn canonicalize_path(engine_state: &EngineState, path: &Path) -> PathBuf {
|
fn canonicalize_path(engine_state: &EngineState, path: &Path) -> PathBuf {
|
||||||
|
@ -307,12 +310,44 @@ pub fn eval_constant(
|
||||||
}
|
}
|
||||||
Ok(Value::list(output, expr.span))
|
Ok(Value::list(output, expr.span))
|
||||||
}
|
}
|
||||||
Expr::Record(fields) => {
|
Expr::Record(items) => {
|
||||||
let mut record = Record::new();
|
let mut record = Record::new();
|
||||||
for (col, val) in fields {
|
let mut col_names = HashMap::new();
|
||||||
// avoid duplicate cols.
|
for item in items {
|
||||||
let col_name = value_as_string(eval_constant(working_set, col)?, expr.span)?;
|
match item {
|
||||||
record.insert(col_name, eval_constant(working_set, val)?);
|
RecordItem::Pair(col, val) => {
|
||||||
|
// avoid duplicate cols
|
||||||
|
let col_name =
|
||||||
|
value_as_string(eval_constant(working_set, col)?, expr.span)?;
|
||||||
|
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_constant(working_set, val)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RecordItem::Spread(_, inner) => match eval_constant(working_set, 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 }),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Value::record(record, expr.span))
|
Ok(Value::record(record, expr.span))
|
||||||
|
@ -326,6 +361,7 @@ pub fn eval_constant(
|
||||||
.position(|existing| existing == &header)
|
.position(|existing| existing == &header)
|
||||||
{
|
{
|
||||||
return Err(ShellError::ColumnDefinedTwice {
|
return Err(ShellError::ColumnDefinedTwice {
|
||||||
|
col_name: header,
|
||||||
second_use: expr.span,
|
second_use: expr.span,
|
||||||
first_use: headers[idx].span,
|
first_use: headers[idx].span,
|
||||||
});
|
});
|
||||||
|
|
|
@ -597,9 +597,10 @@ pub enum ShellError {
|
||||||
/// ## Resolution
|
/// ## Resolution
|
||||||
///
|
///
|
||||||
/// Check the record to ensure you aren't reusing the same field name
|
/// Check the record to ensure you aren't reusing the same field name
|
||||||
#[error("Record field or table column used twice")]
|
#[error("Record field or table column used twice: {col_name}")]
|
||||||
#[diagnostic(code(nu::shell::column_defined_twice))]
|
#[diagnostic(code(nu::shell::column_defined_twice))]
|
||||||
ColumnDefinedTwice {
|
ColumnDefinedTwice {
|
||||||
|
col_name: String,
|
||||||
#[label = "field redefined here"]
|
#[label = "field redefined here"]
|
||||||
second_use: Span,
|
second_use: Span,
|
||||||
#[label = "field first defined here"]
|
#[label = "field first defined here"]
|
||||||
|
@ -1213,13 +1214,28 @@ This is an internal Nushell error, please file an issue https://github.com/nushe
|
||||||
/// Only lists can be spread inside lists. Try converting the value to a list before spreading.
|
/// Only lists can be spread inside lists. Try converting the value to a list before spreading.
|
||||||
#[error("Not a list")]
|
#[error("Not a list")]
|
||||||
#[diagnostic(
|
#[diagnostic(
|
||||||
code(nu::shell::cannot_spread),
|
code(nu::shell::cannot_spread_as_list),
|
||||||
help("Only lists can be spread inside lists. Try converting the value to a list before spreading")
|
help("Only lists can be spread inside lists. Try converting the value to a list before spreading")
|
||||||
)]
|
)]
|
||||||
CannotSpreadAsList {
|
CannotSpreadAsList {
|
||||||
#[label = "cannot spread value"]
|
#[label = "cannot spread value"]
|
||||||
span: Span,
|
span: Span,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Tried spreading a non-record inside a record.
|
||||||
|
///
|
||||||
|
/// ## Resolution
|
||||||
|
///
|
||||||
|
/// Only records can be spread inside records. Try converting the value to a record before spreading.
|
||||||
|
#[error("Not a record")]
|
||||||
|
#[diagnostic(
|
||||||
|
code(nu::shell::cannot_spread_as_record),
|
||||||
|
help("Only records can be spread inside records. Try converting the value to a record before spreading.")
|
||||||
|
)]
|
||||||
|
CannotSpreadAsRecord {
|
||||||
|
#[label = "cannot spread value"]
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement as From trait
|
// TODO: Implement as From trait
|
||||||
|
|
|
@ -24,6 +24,30 @@ fn spread_in_list() -> TestResult {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn const_spread_in_list() -> TestResult {
|
||||||
|
run_test(r#"const x = [...[]]; $x | to nuon"#, "[]").unwrap();
|
||||||
|
run_test(
|
||||||
|
r#"const x = [1 2 ...[[3] {x: 1}] 5]; $x | to nuon"#,
|
||||||
|
"[1, 2, [3], {x: 1}, 5]",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
run_test(
|
||||||
|
r#"const x = [...([f o o]) 10]; $x | to nuon"#,
|
||||||
|
"[f, o, o, 10]",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
run_test(
|
||||||
|
r#"const l = [1, 2, [3]]; const x = [...$l $l]; $x | to nuon"#,
|
||||||
|
"[1, 2, [3], [1, 2, [3]]]",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
run_test(
|
||||||
|
r#"[ ...[ ...[ ...[ a ] b ] c ] d ] | to nuon"#,
|
||||||
|
"[a, b, c, d]",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn not_spread() -> TestResult {
|
fn not_spread() -> TestResult {
|
||||||
run_test(r#"def ... [x] { $x }; ... ..."#, "...").unwrap();
|
run_test(r#"def ... [x] { $x }; ... ..."#, "...").unwrap();
|
||||||
|
@ -40,9 +64,78 @@ fn bad_spread_on_non_list() -> TestResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn spread_type() -> TestResult {
|
fn spread_type_list() -> TestResult {
|
||||||
run_test(r#"[1 ...[]] | describe"#, "list<int>").unwrap();
|
run_test(
|
||||||
run_test(r#"[1 ...[2]] | describe"#, "list<int>").unwrap();
|
r#"def f [a: list<int>] { $a | describe }; f [1 ...[]]"#,
|
||||||
run_test(r#"["foo" ...[4 5 6]] | describe"#, "list<any>").unwrap();
|
"list<int>",
|
||||||
run_test(r#"[1 2 ...["misfit"] 4] | describe"#, "list<any>")
|
)
|
||||||
|
.unwrap();
|
||||||
|
run_test(
|
||||||
|
r#"def f [a: list<int>] { $a | describe }; f [1 ...[2]]"#,
|
||||||
|
"list<int>",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
fail_test(
|
||||||
|
r#"def f [a: list<int>] { }; f ["foo" ...[4 5 6]]"#,
|
||||||
|
"expected int",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
fail_test(
|
||||||
|
r#"def f [a: list<int>] { }; f [1 2 ...["misfit"] 4]"#,
|
||||||
|
"expected int",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spread_in_record() -> TestResult {
|
||||||
|
run_test(r#"{...{...{...{}}}} | to nuon"#, "{}").unwrap();
|
||||||
|
run_test(
|
||||||
|
r#"{foo: bar ...{a: {x: 1}} b: 3} | to nuon"#,
|
||||||
|
"{foo: bar, a: {x: 1}, b: 3}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn const_spread_in_record() -> TestResult {
|
||||||
|
run_test(r#"const x = {...{...{...{}}}}; $x | to nuon"#, "{}").unwrap();
|
||||||
|
run_test(
|
||||||
|
r#"const x = {foo: bar ...{a: {x: 1}} b: 3}; $x | to nuon"#,
|
||||||
|
"{foo: bar, a: {x: 1}, b: 3}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_cols() -> TestResult {
|
||||||
|
fail_test(r#"{a: 1, ...{a: 3}}"#, "column used twice").unwrap();
|
||||||
|
fail_test(r#"{...{a: 4, x: 3}, x: 1}"#, "column used twice").unwrap();
|
||||||
|
fail_test(r#"{...{a: 0, x: 2}, ...{x: 5}}"#, "column used twice")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn const_duplicate_cols() -> TestResult {
|
||||||
|
fail_test(r#"const _ = {a: 1, ...{a: 3}}"#, "column used twice").unwrap();
|
||||||
|
fail_test(r#"const _ = {...{a: 4, x: 3}, x: 1}"#, "column used twice").unwrap();
|
||||||
|
fail_test(
|
||||||
|
r#"const _ = {...{a: 0, x: 2}, ...{x: 5}}"#,
|
||||||
|
"column used twice",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bad_spread_on_non_record() -> TestResult {
|
||||||
|
fail_test(r#"let x = 5; { ...$x }"#, "cannot spread").unwrap();
|
||||||
|
fail_test(r#"{...([1, 2])}"#, "cannot spread")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spread_type_record() -> TestResult {
|
||||||
|
run_test(
|
||||||
|
r#"def f [a: record<x: int>] { $a.x }; f { ...{x: 0} }"#,
|
||||||
|
"0",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
fail_test(
|
||||||
|
r#"def f [a: record<x: int>] {}; f { ...{x: "not an int"} }"#,
|
||||||
|
"type_mismatch",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue