Skip to content

Commit a435881

Browse files
committed
Implement record spreading in const values
1 parent 6b36ea4 commit a435881

18 files changed

+1510
-12
lines changed

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

15131513
Constant::Record { tag, arguments, .. } => {
1514-
let arguments = arguments
1514+
// Spreads are fully expanded during type checking, so we just handle arguments
1515+
let arguments_doc = arguments
15151516
.iter()
15161517
.map(|argument| const_inline(&argument.value, env));
15171518
let tag = atom_string(to_snake_case(tag));
1518-
tuple(std::iter::once(tag).chain(arguments))
1519+
tuple(std::iter::once(tag).chain(arguments_doc))
15191520
}
15201521

15211522
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
@@ -3292,23 +3292,54 @@ where
32923292
) -> Result<Option<UntypedConstant>, ParseError> {
32933293
match self.maybe_one(&Token::LeftParen) {
32943294
Some((par_s, _)) => {
3295-
let arguments =
3296-
Parser::series_of(self, &Parser::parse_const_record_arg, Some(&Token::Comma))?;
3295+
// Check for spread syntax: Record(..base, ...)
3296+
let spread = match self.maybe_one(&Token::DotDot) {
3297+
Some(_) => {
3298+
// Parse the spread target constant
3299+
let spread_value = self.parse_const_value()?;
3300+
match spread_value {
3301+
Some(value) => Some(Box::new(value)),
3302+
None => {
3303+
return parse_error(
3304+
ParseErrorType::UnexpectedEof,
3305+
SrcSpan::new(par_s, par_s + 2),
3306+
);
3307+
}
3308+
}
3309+
}
3310+
None => None,
3311+
};
3312+
3313+
// Parse remaining arguments after the spread (if any)
3314+
let mut arguments = vec![];
3315+
if (spread.is_some() && self.maybe_one(&Token::Comma).is_some()) || spread.is_none()
3316+
{
3317+
arguments = Parser::series_of(
3318+
self,
3319+
&Parser::parse_const_record_arg,
3320+
Some(&Token::Comma),
3321+
)?;
3322+
}
3323+
32973324
let (_, par_e) = self.expect_one_following_series(
32983325
&Token::RightParen,
32993326
"a constant record argument",
33003327
)?;
3301-
if arguments.is_empty() {
3328+
3329+
// Validate that we have either arguments or a spread
3330+
if arguments.is_empty() && spread.is_none() {
33023331
return parse_error(
33033332
ParseErrorType::ConstantRecordConstructorNoArguments,
33043333
SrcSpan::new(par_s, par_e),
33053334
);
33063335
}
3336+
33073337
Ok(Some(Constant::Record {
33083338
location: SrcSpan { start, end: par_e },
33093339
module,
33103340
name,
33113341
arguments,
3342+
spread,
33123343
tag: (),
33133344
type_: (),
33143345
field_map: None,
@@ -3320,6 +3351,7 @@ where
33203351
module,
33213352
name,
33223353
arguments: vec![],
3354+
spread: None,
33233355
tag: (),
33243356
type_: (),
33253357
field_map: None,

0 commit comments

Comments
 (0)