Skip to content

Commit df799f4

Browse files
committed
Implement record spreading in const values
1 parent da995eb commit df799f4

19 files changed

+1524
-12
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@
2929

3030
([Surya Rose](https://github.com/GearsDatapacks))
3131

32+
- Record update syntax now works with spreading records in type constructors
33+
for const values:
34+
```gleam
35+
const a = Foo(1, 2)
36+
const b = Foo(..a, 3)
37+
```
38+
([Adi Salimgereyev](https://github.com/abs0luty))
39+
3240
### Build tool
3341

3442
- 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
@@ -39,6 +39,7 @@ pub enum Constant<T, RecordTag> {
3939
module: Option<(EcoString, SrcSpan)>,
4040
name: EcoString,
4141
arguments: Vec<CallArg<Self>>,
42+
spread: Option<Box<Self>>,
4243
tag: RecordTag,
4344
type_: T,
4445
field_map: Option<FieldMap>,
@@ -103,9 +104,12 @@ impl TypedConstant {
103104
.iter()
104105
.find_map(|element| element.find_node(byte_index))
105106
.unwrap_or(Located::Constant(self)),
106-
Constant::Record { arguments, .. } => arguments
107+
Constant::Record {
108+
arguments, spread, ..
109+
} => arguments
107110
.iter()
108111
.find_map(|argument| argument.find_node(byte_index))
112+
.or_else(|| spread.as_ref().and_then(|s| s.find_node(byte_index)))
109113
.unwrap_or(Located::Constant(self)),
110114
Constant::BitArray { segments, .. } => segments
111115
.iter()
@@ -155,10 +159,18 @@ impl TypedConstant {
155159
.map(|element| element.referenced_variables())
156160
.fold(im::hashset![], im::HashSet::union),
157161

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

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

compiler-core/src/ast_folder.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -954,11 +954,12 @@ pub trait UntypedConstantFolder {
954954
module,
955955
name,
956956
arguments,
957+
spread,
957958
tag: (),
958959
type_: (),
959960
field_map: _,
960961
record_constructor: _,
961-
} => self.fold_constant_record(location, module, name, arguments),
962+
} => self.fold_constant_record(location, module, name, arguments, spread),
962963

963964
Constant::BitArray { location, segments } => {
964965
self.fold_constant_bit_array(location, segments)
@@ -1032,12 +1033,14 @@ pub trait UntypedConstantFolder {
10321033
module: Option<(EcoString, SrcSpan)>,
10331034
name: EcoString,
10341035
arguments: Vec<CallArg<UntypedConstant>>,
1036+
spread: Option<Box<UntypedConstant>>,
10351037
) -> UntypedConstant {
10361038
Constant::Record {
10371039
location,
10381040
module,
10391041
name,
10401042
arguments,
1043+
spread,
10411044
tag: (),
10421045
type_: (),
10431046
field_map: None,
@@ -1119,6 +1122,7 @@ pub trait UntypedConstantFolder {
11191122
module,
11201123
name,
11211124
arguments,
1125+
spread,
11221126
tag,
11231127
type_,
11241128
field_map,
@@ -1131,11 +1135,13 @@ pub trait UntypedConstantFolder {
11311135
argument
11321136
})
11331137
.collect();
1138+
let spread = spread.map(|s| Box::new(self.fold_constant(*s)));
11341139
Constant::Record {
11351140
location,
11361141
module,
11371142
name,
11381143
arguments,
1144+
spread,
11391145
tag,
11401146
type_,
11411147
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
@@ -434,6 +434,7 @@ impl ModuleDecoder {
434434
module: Default::default(),
435435
name: Default::default(),
436436
arguments,
437+
spread: None,
437438
tag,
438439
type_,
439440
field_map: None,

compiler-core/src/metadata/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,6 +1159,7 @@ fn constant_record() {
11591159
},
11601160
},
11611161
],
1162+
spread: None,
11621163
tag: "thetag".into(),
11631164
type_: type_::int(),
11641165
field_map: None,

compiler-core/src/parse.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3313,23 +3313,54 @@ where
33133313
) -> Result<Option<UntypedConstant>, ParseError> {
33143314
match self.maybe_one(&Token::LeftParen) {
33153315
Some((par_s, _)) => {
3316-
let arguments =
3317-
Parser::series_of(self, &Parser::parse_const_record_arg, Some(&Token::Comma))?;
3316+
// Check for spread syntax: Record(..base, ...)
3317+
let spread = match self.maybe_one(&Token::DotDot) {
3318+
Some(_) => {
3319+
// Parse the spread target constant
3320+
let spread_value = self.parse_const_value()?;
3321+
match spread_value {
3322+
Some(value) => Some(Box::new(value)),
3323+
None => {
3324+
return parse_error(
3325+
ParseErrorType::UnexpectedEof,
3326+
SrcSpan::new(par_s, par_s + 2),
3327+
);
3328+
}
3329+
}
3330+
}
3331+
None => None,
3332+
};
3333+
3334+
// Parse remaining arguments after the spread (if any)
3335+
let mut arguments = vec![];
3336+
if (spread.is_some() && self.maybe_one(&Token::Comma).is_some()) || spread.is_none()
3337+
{
3338+
arguments = Parser::series_of(
3339+
self,
3340+
&Parser::parse_const_record_arg,
3341+
Some(&Token::Comma),
3342+
)?;
3343+
}
3344+
33183345
let (_, par_e) = self.expect_one_following_series(
33193346
&Token::RightParen,
33203347
"a constant record argument",
33213348
)?;
3322-
if arguments.is_empty() {
3349+
3350+
// Validate that we have either arguments or a spread
3351+
if arguments.is_empty() && spread.is_none() {
33233352
return parse_error(
33243353
ParseErrorType::ConstantRecordConstructorNoArguments,
33253354
SrcSpan::new(par_s, par_e),
33263355
);
33273356
}
3357+
33283358
Ok(Some(Constant::Record {
33293359
location: SrcSpan { start, end: par_e },
33303360
module,
33313361
name,
33323362
arguments,
3363+
spread,
33333364
tag: (),
33343365
type_: (),
33353366
field_map: None,
@@ -3341,6 +3372,7 @@ where
33413372
module,
33423373
name,
33433374
arguments: vec![],
3375+
spread: None,
33443376
tag: (),
33453377
type_: (),
33463378
field_map: None,

0 commit comments

Comments
 (0)