Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@
containing scientific notation or trailing zeros (i.e. `100` and `1e2`).
([ptdewey](https://github.com/ptdewey))

- Record update syntax now works with spreading records in type constructors
for const values:
```gleam
const a = Foo(1, 2)
const b = Foo(..a, 3)
```
([Adi Salimgereyev](https://github.com/abs0luty))

### Build tool

- The help text displayed by `gleam dev --help`, `gleam test --help`, and
Expand Down
22 changes: 17 additions & 5 deletions compiler-core/src/ast/constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub enum Constant<T, RecordTag> {
module: Option<(EcoString, SrcSpan)>,
name: EcoString,
arguments: Vec<CallArg<Self>>,
spread: Option<Box<Self>>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For non-constant expressions record updates have their own AST node, is there a reason not to do that here? It would mean that the overrides have to be labelled, which is nice to enforce.

tag: RecordTag,
type_: T,
field_map: Option<FieldMap>,
Expand Down Expand Up @@ -104,9 +105,12 @@ impl TypedConstant {
.iter()
.find_map(|element| element.find_node(byte_index))
.unwrap_or(Located::Constant(self)),
Constant::Record { arguments, .. } => arguments
Constant::Record {
arguments, spread, ..
} => arguments
.iter()
.find_map(|argument| argument.find_node(byte_index))
.or_else(|| spread.as_ref().and_then(|s| s.find_node(byte_index)))
.unwrap_or(Located::Constant(self)),
Constant::BitArray { segments, .. } => segments
.iter()
Expand Down Expand Up @@ -156,10 +160,18 @@ impl TypedConstant {
.map(|element| element.referenced_variables())
.fold(im::hashset![], im::HashSet::union),

Constant::Record { arguments, .. } => arguments
.iter()
.map(|argument| argument.value.referenced_variables())
.fold(im::hashset![], im::HashSet::union),
Constant::Record {
arguments, spread, ..
} => {
let arg_vars = arguments
.iter()
.map(|argument| argument.value.referenced_variables())
.fold(im::hashset![], im::HashSet::union);
match spread {
Some(spread) => arg_vars.union(spread.referenced_variables()),
None => arg_vars,
}
}

Constant::BitArray { segments, .. } => segments
.iter()
Expand Down
8 changes: 7 additions & 1 deletion compiler-core/src/ast_folder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -972,11 +972,12 @@ pub trait UntypedConstantFolder {
module,
name,
arguments,
spread,
tag: (),
type_: (),
field_map: _,
record_constructor: _,
} => self.fold_constant_record(location, module, name, arguments),
} => self.fold_constant_record(location, module, name, arguments, spread),

Constant::BitArray { location, segments } => {
self.fold_constant_bit_array(location, segments)
Expand Down Expand Up @@ -1059,12 +1060,14 @@ pub trait UntypedConstantFolder {
module: Option<(EcoString, SrcSpan)>,
name: EcoString,
arguments: Vec<CallArg<UntypedConstant>>,
spread: Option<Box<UntypedConstant>>,
) -> UntypedConstant {
Constant::Record {
location,
module,
name,
arguments,
spread,
tag: (),
type_: (),
field_map: None,
Expand Down Expand Up @@ -1146,6 +1149,7 @@ pub trait UntypedConstantFolder {
module,
name,
arguments,
spread,
tag,
type_,
field_map,
Expand All @@ -1158,11 +1162,13 @@ pub trait UntypedConstantFolder {
argument
})
.collect();
let spread = spread.map(|s| Box::new(self.fold_constant(*s)));
Constant::Record {
location,
module,
name,
arguments,
spread,
tag,
type_,
field_map,
Expand Down
5 changes: 3 additions & 2 deletions compiler-core/src/erlang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1542,11 +1542,12 @@ fn const_inline<'a>(literal: &'a TypedConstant, env: &mut Env<'a>) -> Document<'
},

Constant::Record { tag, arguments, .. } => {
let arguments = arguments
// Spreads are fully expanded during type checking, so we just handle arguments
let arguments_doc = arguments
.iter()
.map(|argument| const_inline(&argument.value, env));
let tag = atom_string(to_snake_case(tag));
tuple(std::iter::once(tag).chain(arguments))
tuple(std::iter::once(tag).chain(arguments_doc))
}

Constant::Var {
Expand Down
2 changes: 2 additions & 0 deletions compiler-core/src/javascript/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1839,6 +1839,7 @@ impl<'module, 'a> Generator<'module, 'a> {
return record_constructor(type_.clone(), None, name, arity, self.tracker);
}

// Spreads are fully expanded during type checking, so we just handle arguments
let field_values = arguments
.iter()
.map(|argument| self.constant_expression(context, &argument.value))
Expand Down Expand Up @@ -2253,6 +2254,7 @@ impl<'module, 'a> Generator<'module, 'a> {
return record_constructor(type_.clone(), None, name, arity, self.tracker);
}

// Spreads are fully expanded during type checking, so we just handle arguments
let field_values = arguments
.iter()
.map(|argument| self.guard_constant_expression(&argument.value))
Expand Down
1 change: 1 addition & 0 deletions compiler-core/src/metadata/module_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ impl ModuleDecoder {
module: Default::default(),
name: Default::default(),
arguments,
spread: None,
tag,
type_,
field_map: None,
Expand Down
1 change: 1 addition & 0 deletions compiler-core/src/metadata/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,7 @@ fn constant_record() {
},
},
],
spread: None,
tag: "thetag".into(),
type_: type_::int(),
field_map: None,
Expand Down
38 changes: 35 additions & 3 deletions compiler-core/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3310,23 +3310,54 @@ where
) -> Result<Option<UntypedConstant>, ParseError> {
match self.maybe_one(&Token::LeftParen) {
Some((par_s, _)) => {
let arguments =
Parser::series_of(self, &Parser::parse_const_record_arg, Some(&Token::Comma))?;
// Check for spread syntax: Record(..base, ...)
let spread = match self.maybe_one(&Token::DotDot) {
Some(_) => {
// Parse the spread target constant
let spread_value = self.parse_const_value()?;
match spread_value {
Some(value) => Some(Box::new(value)),
None => {
return parse_error(
ParseErrorType::UnexpectedEof,
SrcSpan::new(par_s, par_s + 2),
);
}
}
}
None => None,
};

// Parse remaining arguments after the spread (if any)
let mut arguments = vec![];
if (spread.is_some() && self.maybe_one(&Token::Comma).is_some()) || spread.is_none()
{
arguments = Parser::series_of(
self,
&Parser::parse_const_record_arg,
Some(&Token::Comma),
)?;
}

let (_, par_e) = self.expect_one_following_series(
&Token::RightParen,
"a constant record argument",
)?;
if arguments.is_empty() {

// Validate that we have either arguments or a spread
if arguments.is_empty() && spread.is_none() {
return parse_error(
ParseErrorType::ConstantRecordConstructorNoArguments,
SrcSpan::new(par_s, par_e),
);
}

Ok(Some(Constant::Record {
location: SrcSpan { start, end: par_e },
module,
name,
arguments,
spread,
tag: (),
type_: (),
field_map: None,
Expand All @@ -3338,6 +3369,7 @@ where
module,
name,
arguments: vec![],
spread: None,
tag: (),
type_: (),
field_map: None,
Expand Down
Loading
Loading