Skip to content

Commit 5d39a71

Browse files
committed
Implement record spreading in const values
1 parent 69890d3 commit 5d39a71

19 files changed

+1524
-12
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@
3434
than stopping at the syntax error.
3535
([mxtthias](https://github.com/mxtthias))
3636

37+
- Record update syntax now works with spreading records in type constructors
38+
for const values:
39+
```gleam
40+
const a = Foo(1, 2)
41+
const b = Foo(..a, 3)
42+
```
43+
([Adi Salimgereyev](https://github.com/abs0luty))
44+
3745
### Build tool
3846

3947
- 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
@@ -1529,11 +1529,12 @@ fn const_inline<'a>(literal: &'a TypedConstant, env: &mut Env<'a>) -> Document<'
15291529
},
15301530

15311531
Constant::Record { tag, arguments, .. } => {
1532-
let arguments = arguments
1532+
// Spreads are fully expanded during type checking, so we just handle arguments
1533+
let arguments_doc = arguments
15331534
.iter()
15341535
.map(|argument| const_inline(&argument.value, env));
15351536
let tag = atom_string(to_snake_case(tag));
1536-
tuple(std::iter::once(tag).chain(arguments))
1537+
tuple(std::iter::once(tag).chain(arguments_doc))
15371538
}
15381539

15391540
Constant::Var {

compiler-core/src/javascript/expression.rs

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

1781+
// Spreads are fully expanded during type checking, so we just handle arguments
17811782
let field_values = arguments
17821783
.iter()
17831784
.map(|argument| self.constant_expression(context, &argument.value))
@@ -2163,6 +2164,7 @@ impl<'module, 'a> Generator<'module, 'a> {
21632164
return record_constructor(type_.clone(), None, name, arity, self.tracker);
21642165
}
21652166

2167+
// Spreads are fully expanded during type checking, so we just handle arguments
21662168
let field_values = arguments
21672169
.iter()
21682170
.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
@@ -3308,23 +3308,54 @@ where
33083308
) -> Result<Option<UntypedConstant>, ParseError> {
33093309
match self.maybe_one(&Token::LeftParen) {
33103310
Some((par_s, _)) => {
3311-
let arguments =
3312-
Parser::series_of(self, &Parser::parse_const_record_arg, Some(&Token::Comma))?;
3311+
// Check for spread syntax: Record(..base, ...)
3312+
let spread = match self.maybe_one(&Token::DotDot) {
3313+
Some(_) => {
3314+
// Parse the spread target constant
3315+
let spread_value = self.parse_const_value()?;
3316+
match spread_value {
3317+
Some(value) => Some(Box::new(value)),
3318+
None => {
3319+
return parse_error(
3320+
ParseErrorType::UnexpectedEof,
3321+
SrcSpan::new(par_s, par_s + 2),
3322+
);
3323+
}
3324+
}
3325+
}
3326+
None => None,
3327+
};
3328+
3329+
// Parse remaining arguments after the spread (if any)
3330+
let mut arguments = vec![];
3331+
if (spread.is_some() && self.maybe_one(&Token::Comma).is_some()) || spread.is_none()
3332+
{
3333+
arguments = Parser::series_of(
3334+
self,
3335+
&Parser::parse_const_record_arg,
3336+
Some(&Token::Comma),
3337+
)?;
3338+
}
3339+
33133340
let (_, par_e) = self.expect_one_following_series(
33143341
&Token::RightParen,
33153342
"a constant record argument",
33163343
)?;
3317-
if arguments.is_empty() {
3344+
3345+
// Validate that we have either arguments or a spread
3346+
if arguments.is_empty() && spread.is_none() {
33183347
return parse_error(
33193348
ParseErrorType::ConstantRecordConstructorNoArguments,
33203349
SrcSpan::new(par_s, par_e),
33213350
);
33223351
}
3352+
33233353
Ok(Some(Constant::Record {
33243354
location: SrcSpan { start, end: par_e },
33253355
module,
33263356
name,
33273357
arguments,
3358+
spread,
33283359
tag: (),
33293360
type_: (),
33303361
field_map: None,
@@ -3336,6 +3367,7 @@ where
33363367
module,
33373368
name,
33383369
arguments: vec![],
3370+
spread: None,
33393371
tag: (),
33403372
type_: (),
33413373
field_map: None,

0 commit comments

Comments
 (0)