Skip to content

Commit 2c0458f

Browse files
committed
Implement record spreading in const values
1 parent afcd7e9 commit 2c0458f

19 files changed

+1524
-12
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@
5151
containing scientific notation or trailing zeros (i.e. `100` and `1e2`).
5252
([ptdewey](https://github.com/ptdewey))
5353

54+
- Record update syntax now works with spreading records in type constructors
55+
for const values:
56+
```gleam
57+
const a = Foo(1, 2)
58+
const b = Foo(..a, 3)
59+
```
60+
([Adi Salimgereyev](https://github.com/abs0luty))
61+
5462
### Build tool
5563

5664
- The help text displayed by `gleam dev --help`, `gleam test --help`, and

compiler-core/src/ast/constant.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub enum Constant<T, RecordTag> {
4040
module: Option<(EcoString, SrcSpan)>,
4141
name: EcoString,
4242
arguments: Vec<CallArg<Self>>,
43+
spread: Option<Box<Self>>,
4344
tag: RecordTag,
4445
type_: T,
4546
field_map: Option<FieldMap>,
@@ -104,9 +105,12 @@ impl TypedConstant {
104105
.iter()
105106
.find_map(|element| element.find_node(byte_index))
106107
.unwrap_or(Located::Constant(self)),
107-
Constant::Record { arguments, .. } => arguments
108+
Constant::Record {
109+
arguments, spread, ..
110+
} => arguments
108111
.iter()
109112
.find_map(|argument| argument.find_node(byte_index))
113+
.or_else(|| spread.as_ref().and_then(|s| s.find_node(byte_index)))
110114
.unwrap_or(Located::Constant(self)),
111115
Constant::BitArray { segments, .. } => segments
112116
.iter()
@@ -156,10 +160,18 @@ impl TypedConstant {
156160
.map(|element| element.referenced_variables())
157161
.fold(im::hashset![], im::HashSet::union),
158162

159-
Constant::Record { arguments, .. } => arguments
160-
.iter()
161-
.map(|argument| argument.value.referenced_variables())
162-
.fold(im::hashset![], im::HashSet::union),
163+
Constant::Record {
164+
arguments, spread, ..
165+
} => {
166+
let arg_vars = arguments
167+
.iter()
168+
.map(|argument| argument.value.referenced_variables())
169+
.fold(im::hashset![], im::HashSet::union);
170+
match spread {
171+
Some(spread) => arg_vars.union(spread.referenced_variables()),
172+
None => arg_vars,
173+
}
174+
}
163175

164176
Constant::BitArray { segments, .. } => segments
165177
.iter()

compiler-core/src/ast_folder.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -972,11 +972,12 @@ pub trait UntypedConstantFolder {
972972
module,
973973
name,
974974
arguments,
975+
spread,
975976
tag: (),
976977
type_: (),
977978
field_map: _,
978979
record_constructor: _,
979-
} => self.fold_constant_record(location, module, name, arguments),
980+
} => self.fold_constant_record(location, module, name, arguments, spread),
980981

981982
Constant::BitArray { location, segments } => {
982983
self.fold_constant_bit_array(location, segments)
@@ -1059,12 +1060,14 @@ pub trait UntypedConstantFolder {
10591060
module: Option<(EcoString, SrcSpan)>,
10601061
name: EcoString,
10611062
arguments: Vec<CallArg<UntypedConstant>>,
1063+
spread: Option<Box<UntypedConstant>>,
10621064
) -> UntypedConstant {
10631065
Constant::Record {
10641066
location,
10651067
module,
10661068
name,
10671069
arguments,
1070+
spread,
10681071
tag: (),
10691072
type_: (),
10701073
field_map: None,
@@ -1146,6 +1149,7 @@ pub trait UntypedConstantFolder {
11461149
module,
11471150
name,
11481151
arguments,
1152+
spread,
11491153
tag,
11501154
type_,
11511155
field_map,
@@ -1158,11 +1162,13 @@ pub trait UntypedConstantFolder {
11581162
argument
11591163
})
11601164
.collect();
1165+
let spread = spread.map(|s| Box::new(self.fold_constant(*s)));
11611166
Constant::Record {
11621167
location,
11631168
module,
11641169
name,
11651170
arguments,
1171+
spread,
11661172
tag,
11671173
type_,
11681174
field_map,

compiler-core/src/erlang.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,11 +1542,12 @@ fn const_inline<'a>(literal: &'a TypedConstant, env: &mut Env<'a>) -> Document<'
15421542
},
15431543

15441544
Constant::Record { tag, arguments, .. } => {
1545-
let arguments = arguments
1545+
// Spreads are fully expanded during type checking, so we just handle arguments
1546+
let arguments_doc = arguments
15461547
.iter()
15471548
.map(|argument| const_inline(&argument.value, env));
15481549
let tag = atom_string(to_snake_case(tag));
1549-
tuple(std::iter::once(tag).chain(arguments))
1550+
tuple(std::iter::once(tag).chain(arguments_doc))
15501551
}
15511552

15521553
Constant::Var {

compiler-core/src/javascript/expression.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1839,6 +1839,7 @@ impl<'module, 'a> Generator<'module, 'a> {
18391839
return record_constructor(type_.clone(), None, name, arity, self.tracker);
18401840
}
18411841

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

2257+
// Spreads are fully expanded during type checking, so we just handle arguments
22562258
let field_values = arguments
22572259
.iter()
22582260
.map(|argument| self.guard_constant_expression(&argument.value))

compiler-core/src/metadata/module_decoder.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ impl ModuleDecoder {
437437
module: Default::default(),
438438
name: Default::default(),
439439
arguments,
440+
spread: None,
440441
tag,
441442
type_,
442443
field_map: None,

compiler-core/src/metadata/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,7 @@ fn constant_record() {
11641164
},
11651165
},
11661166
],
1167+
spread: None,
11671168
tag: "thetag".into(),
11681169
type_: type_::int(),
11691170
field_map: None,

compiler-core/src/parse.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3310,23 +3310,54 @@ where
33103310
) -> Result<Option<UntypedConstant>, ParseError> {
33113311
match self.maybe_one(&Token::LeftParen) {
33123312
Some((par_s, _)) => {
3313-
let arguments =
3314-
Parser::series_of(self, &Parser::parse_const_record_arg, Some(&Token::Comma))?;
3313+
// Check for spread syntax: Record(..base, ...)
3314+
let spread = match self.maybe_one(&Token::DotDot) {
3315+
Some(_) => {
3316+
// Parse the spread target constant
3317+
let spread_value = self.parse_const_value()?;
3318+
match spread_value {
3319+
Some(value) => Some(Box::new(value)),
3320+
None => {
3321+
return parse_error(
3322+
ParseErrorType::UnexpectedEof,
3323+
SrcSpan::new(par_s, par_s + 2),
3324+
);
3325+
}
3326+
}
3327+
}
3328+
None => None,
3329+
};
3330+
3331+
// Parse remaining arguments after the spread (if any)
3332+
let mut arguments = vec![];
3333+
if (spread.is_some() && self.maybe_one(&Token::Comma).is_some()) || spread.is_none()
3334+
{
3335+
arguments = Parser::series_of(
3336+
self,
3337+
&Parser::parse_const_record_arg,
3338+
Some(&Token::Comma),
3339+
)?;
3340+
}
3341+
33153342
let (_, par_e) = self.expect_one_following_series(
33163343
&Token::RightParen,
33173344
"a constant record argument",
33183345
)?;
3319-
if arguments.is_empty() {
3346+
3347+
// Validate that we have either arguments or a spread
3348+
if arguments.is_empty() && spread.is_none() {
33203349
return parse_error(
33213350
ParseErrorType::ConstantRecordConstructorNoArguments,
33223351
SrcSpan::new(par_s, par_e),
33233352
);
33243353
}
3354+
33253355
Ok(Some(Constant::Record {
33263356
location: SrcSpan { start, end: par_e },
33273357
module,
33283358
name,
33293359
arguments,
3360+
spread,
33303361
tag: (),
33313362
type_: (),
33323363
field_map: None,
@@ -3338,6 +3369,7 @@ where
33383369
module,
33393370
name,
33403371
arguments: vec![],
3372+
spread: None,
33413373
tag: (),
33423374
type_: (),
33433375
field_map: None,

0 commit comments

Comments
 (0)