diff --git a/crates/nu-cli/src/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs index 3b7d9bdcb2..dec40c08f5 100644 --- a/crates/nu-cli/src/syntax_highlight.rs +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -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}; @@ -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 } diff --git a/crates/nu-command/src/formats/from/nuon.rs b/crates/nu-command/src/formats/from/nuon.rs index 90f0ecd41d..f57a8a2a38 100644 --- a/crates/nu-command/src/formats/from/nuon.rs +++ b/crates/nu-command/src/formats/from/nuon.rs @@ -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, @@ -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)) @@ -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, }); diff --git a/crates/nu-engine/src/documentation.rs b/crates/nu-engine/src/documentation.rs index f29af455d7..6de0554b28 100644 --- a/crates/nu-engine/src/documentation.rs +++ b/crates/nu-engine/src/documentation.rs @@ -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}, @@ -378,10 +378,10 @@ fn get_argument_for_color_value( ) -> Option { match color { Value::Record { val, .. } => { - let record_exp: Vec<(Expression, Expression)> = val + let record_exp: Vec = val .into_iter() .map(|(k, v)| { - ( + RecordItem::Pair( Expression { expr: Expr::String(k.clone()), span, diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index fed9e451f5..6e3bc90839 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -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, @@ -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 }), + } } } } @@ -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, }); diff --git a/crates/nu-parser/src/flatten.rs b/crates/nu-parser/src/flatten.rs index a64d2c63b7..26cd1e5bf4 100644 --- a/crates/nu-parser/src/flatten.rs +++ b/crates/nu-parser/src/flatten.rs @@ -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}; @@ -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(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_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 { output.push((Span::new(last_end, outer_span.end), FlatShape::Record)); diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 47964c4754..7d685c318f 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -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) => { diff --git a/crates/nu-protocol/src/ast/expr.rs b/crates/nu-protocol/src/ast/expr.rs index b10a7838ba..635c26db41 100644 --- a/crates/nu-protocol/src/ast/expr.rs +++ b/crates/nu-protocol/src/ast/expr.rs @@ -30,7 +30,7 @@ pub enum Expr { MatchBlock(Vec<(MatchPattern, Expression)>), List(Vec), Table(Vec, Vec>), - Record(Vec<(Expression, Expression)>), + Record(Vec), Keyword(Vec, Span, Box), ValueWithUnit(Box, Spanned), DateTime(chrono::DateTime), @@ -49,3 +49,11 @@ pub enum Expr { Nothing, 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), +} diff --git a/crates/nu-protocol/src/ast/expression.rs b/crates/nu-protocol/src/ast/expression.rs index 7a6b1c1a3f..ebd3ce11e6 100644 --- a/crates/nu-protocol/src/ast/expression.rs +++ b/crates/nu-protocol/src/ast/expression.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use super::Expr; +use super::{Expr, RecordItem}; use crate::ast::ImportPattern; use crate::DeclId; use crate::{engine::StateWorkingSet, BlockId, Signature, Span, Type, VarId, IN_VARIABLE_ID}; @@ -242,13 +242,22 @@ impl Expression { } false } - Expr::Record(fields) => { - for (field_name, field_value) in fields { - if field_name.has_in_variable(working_set) { - return true; - } - if field_value.has_in_variable(working_set) { - return true; + Expr::Record(items) => { + for item in items { + match item { + RecordItem::Pair(field_name, field_value) => { + if field_name.has_in_variable(working_set) { + 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 @@ -418,10 +427,17 @@ impl Expression { right.replace_in_variable(working_set, new_var_id) } } - Expr::Record(fields) => { - for (field_name, field_value) in fields { - field_name.replace_in_variable(working_set, new_var_id); - field_value.replace_in_variable(working_set, new_var_id); + Expr::Record(items) => { + for item in items { + match item { + 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(_) => {} @@ -581,10 +597,17 @@ impl Expression { right.replace_span(working_set, replaced, new_span) } } - Expr::Record(fields) => { - for (field_name, field_value) in fields { - field_name.replace_span(working_set, replaced, new_span); - field_value.replace_span(working_set, replaced, new_span); + Expr::Record(items) => { + for item in items { + match item { + 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(_) => {} diff --git a/crates/nu-protocol/src/eval_const.rs b/crates/nu-protocol/src/eval_const.rs index 31e07eee93..4b880a1d75 100644 --- a/crates/nu-protocol/src/eval_const.rs +++ b/crates/nu-protocol/src/eval_const.rs @@ -1,13 +1,16 @@ use crate::{ ast::{ eval_operator, Bits, Block, Boolean, Call, Comparison, Expr, Expression, Math, Operator, - PipelineElement, + PipelineElement, RecordItem, }, engine::{EngineState, StateWorkingSet}, 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 std::path::{Path, PathBuf}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; pub fn create_nu_constant(engine_state: &EngineState, span: Span) -> Result { fn canonicalize_path(engine_state: &EngineState, path: &Path) -> PathBuf { @@ -307,12 +310,44 @@ pub fn eval_constant( } 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 = value_as_string(eval_constant(working_set, col)?, expr.span)?; - record.insert(col_name, eval_constant(working_set, val)?); + let mut col_names = HashMap::new(); + for item in items { + match item { + 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)) @@ -326,6 +361,7 @@ pub fn eval_constant( .position(|existing| existing == &header) { return Err(ShellError::ColumnDefinedTwice { + col_name: header, second_use: expr.span, first_use: headers[idx].span, }); diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 610160aabf..02f18730b4 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -597,9 +597,10 @@ pub enum ShellError { /// ## Resolution /// /// 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))] ColumnDefinedTwice { + col_name: String, #[label = "field redefined here"] second_use: Span, #[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. #[error("Not a list")] #[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") )] CannotSpreadAsList { #[label = "cannot spread value"] 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 diff --git a/src/tests/test_spread.rs b/src/tests/test_spread.rs index 891870eb31..86fb8351df 100644 --- a/src/tests/test_spread.rs +++ b/src/tests/test_spread.rs @@ -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] fn not_spread() -> TestResult { run_test(r#"def ... [x] { $x }; ... ..."#, "...").unwrap(); @@ -40,9 +64,78 @@ fn bad_spread_on_non_list() -> TestResult { } #[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") +fn spread_type_list() -> TestResult { + run_test( + r#"def f [a: list] { $a | describe }; f [1 ...[]]"#, + "list", + ) + .unwrap(); + run_test( + r#"def f [a: list] { $a | describe }; f [1 ...[2]]"#, + "list", + ) + .unwrap(); + fail_test( + r#"def f [a: list] { }; f ["foo" ...[4 5 6]]"#, + "expected int", + ) + .unwrap(); + fail_test( + r#"def f [a: list] { }; 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] { $a.x }; f { ...{x: 0} }"#, + "0", + ) + .unwrap(); + fail_test( + r#"def f [a: record] {}; f { ...{x: "not an int"} }"#, + "type_mismatch", + ) }