Skip to content

Commit fe34ecd

Browse files
authored
Dict spreads (#8369)
* dict spreads * changelog * cleanup * format * update
1 parent 8436751 commit fe34ecd

16 files changed

Lines changed: 496 additions & 69 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
- Rewatch: add `--prod` flag to `build`, `watch`, and `clean` to skip dev-dependencies and dev sources (`"type": "dev"`), enabling builds in environments where dev packages aren't installed (e.g. after `pnpm install --prod`). https://github.com/rescript-lang/rescript/pull/8347
2525
- Add `Dict.assignMany`, `Dict.concat`, `Dict.concatMany`, `Dict.concatAll`, `Array.concatAll` to the stdlib. https://github.com/rescript-lang/rescript/pull/8364
2626
- Implement `for...of` and `for await...of` loops. https://github.com/rescript-lang/rescript/pull/7887
27+
- Add support for dict spreads: `dict{...foo, "bar": 2, ...qux}`. https://github.com/rescript-lang/rescript/pull/8369
2728

2829
#### :bug: Bug fix
2930

compiler/syntax/src/res_comments_table.ml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1488,6 +1488,22 @@ and walk_expression expr t comments =
14881488
attach t.trailing expr.pexp_loc after_expr;
14891489
walk_list (cases |> List.map (fun case -> Case case)) t rest
14901490
(* unary expression: todo use parsetreeviewer *)
1491+
| Pexp_apply _
1492+
when Option.is_some
1493+
(Res_parsetree_viewer.collect_spread_dict_expr_parts expr) -> (
1494+
match Res_parsetree_viewer.collect_spread_dict_expr_parts expr with
1495+
| Some parts ->
1496+
let part_exprs =
1497+
List.map
1498+
(function
1499+
| Res_parsetree_viewer.DictExprRows rows_expr ->
1500+
Expression rows_expr
1501+
| Res_parsetree_viewer.DictExprSpread spread_expr ->
1502+
Expression spread_expr)
1503+
parts
1504+
in
1505+
walk_list part_exprs t comments
1506+
| None -> assert false)
14911507
| Pexp_apply
14921508
{
14931509
funct =

compiler/syntax/src/res_core.ml

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,6 @@ module ErrorMessages = struct
150150
...b}` wouldn't make sense, as `b` would override every field of `a` \
151151
anyway."
152152

153-
let dict_expr_spread = "Dict literals do not support spread (`...`) yet."
154-
155153
let record_field_missing_colon =
156154
"Records use `:` when assigning fields. Example: `{field: value}`"
157155

@@ -285,6 +283,7 @@ let tagged_template_literal_attr =
285283
(Location.mknoloc "res.taggedTemplate", Parsetree.PStr [])
286284

287285
let spread_attr = (Location.mknoloc "res.spread", Parsetree.PStr [])
286+
let dict_spread_attr = (Location.mknoloc "res.dictSpread", Parsetree.PStr [])
288287

289288
type argument = {label: Asttypes.arg_label; expr: Parsetree.expression}
290289

@@ -3505,30 +3504,28 @@ and parse_record_expr_row p :
35053504
None)
35063505
else None
35073506

3508-
and parse_dict_expr_row p =
3507+
and parse_dict_expr_part p =
35093508
match p.Parser.token with
35103509
| DotDotDot ->
3511-
Parser.err p (Diagnostics.message ErrorMessages.dict_expr_spread);
35123510
Parser.next p;
3513-
(* Parse the expr so it's consumed *)
3514-
let _spread_expr = parse_constrained_or_coerced_expr p in
3515-
None
3511+
let spread_expr = parse_constrained_or_coerced_expr p in
3512+
Some (`Spread spread_expr)
35163513
| String s -> (
35173514
let loc = mk_loc p.start_pos p.end_pos in
35183515
Parser.next p;
35193516
let field = Location.mkloc (Longident.Lident s) loc in
35203517
match p.Parser.token with
35213518
| Colon ->
35223519
Parser.next p;
3523-
let fieldExpr = parse_expr p in
3524-
Some (field, fieldExpr)
3520+
let field_expr = parse_expr p in
3521+
Some (`Row (field, field_expr))
35253522
| Equal ->
35263523
Parser.err ~start_pos:p.start_pos ~end_pos:p.end_pos p
35273524
(Diagnostics.message ErrorMessages.dict_field_missing_colon);
35283525
Parser.next p;
3529-
let fieldExpr = parse_expr p in
3530-
Some (field, fieldExpr)
3531-
| _ -> Some (field, Ast_helper.Exp.ident ~loc:field.loc field))
3526+
let field_expr = parse_expr p in
3527+
Some (`Row (field, field_expr))
3528+
| _ -> Some (`Row (field, Ast_helper.Exp.ident ~loc:field.loc field)))
35323529
| _ -> None
35333530

35343531
and parse_record_expr_with_string_keys ~start_pos first_row p =
@@ -4374,9 +4371,9 @@ and parse_list_expr ~start_pos p =
43744371
[(Asttypes.Nolabel, Ast_helper.Exp.array ~loc list_exprs)]
43754372

43764373
and parse_dict_expr ~start_pos p =
4377-
let rows =
4374+
let parts =
43784375
parse_comma_delimited_region ~grammar:Grammar.DictRows ~closing:Rbrace
4379-
~f:parse_dict_expr_row p
4376+
~f:parse_dict_expr_part p
43804377
in
43814378
let loc = mk_loc start_pos p.end_pos in
43824379
let to_key_value_pair
@@ -4393,14 +4390,73 @@ and parse_dict_expr ~start_pos p =
43934390
])
43944391
| _ -> None
43954392
in
4396-
let key_value_pairs = List.filter_map to_key_value_pair rows in
4393+
let dict_rows_loc
4394+
(rows : (Longident.t Location.loc * Parsetree.expression) list) =
4395+
match (rows, List.rev rows) with
4396+
| (first_key, _) :: _, (_, last_expr) :: _ ->
4397+
mk_loc first_key.loc.loc_start last_expr.pexp_loc.loc_end
4398+
| _ -> loc
4399+
in
4400+
let make_dict_chunk ?loc_override rows =
4401+
let chunk_loc =
4402+
match loc_override with
4403+
| Some loc -> loc
4404+
| None -> dict_rows_loc rows
4405+
in
4406+
let key_value_pairs = List.filter_map to_key_value_pair rows in
4407+
Ast_helper.Exp.apply ~loc:chunk_loc
4408+
(Ast_helper.Exp.ident ~loc:chunk_loc
4409+
(Location.mkloc
4410+
(Longident.Ldot (Longident.Lident Primitive_modules.dict, "make"))
4411+
chunk_loc))
4412+
[(Asttypes.Nolabel, Ast_helper.Exp.array ~loc:chunk_loc key_value_pairs)]
4413+
in
4414+
let make_dict_spread target_expr source_parts =
4415+
let spread_ident =
4416+
Ast_helper.Exp.ident ~loc ~attrs:[dict_spread_attr]
4417+
(Location.mkloc
4418+
(Longident.Ldot (Longident.Lident Primitive_modules.dict, "spread"))
4419+
loc)
4420+
in
4421+
Ast_helper.Exp.apply ~loc spread_ident
4422+
[
4423+
(Asttypes.Nolabel, target_expr);
4424+
( Asttypes.Nolabel,
4425+
Ast_helper.Exp.array ~loc
4426+
(List.map
4427+
(function
4428+
| `Rows rows -> make_dict_chunk rows
4429+
| `Spread spread_expr -> spread_expr)
4430+
source_parts) );
4431+
]
4432+
in
4433+
let grouped_parts =
4434+
let rec loop current_rows acc = function
4435+
| [] ->
4436+
let acc =
4437+
match current_rows with
4438+
| [] -> acc
4439+
| rows -> `Rows (List.rev rows) :: acc
4440+
in
4441+
List.rev acc
4442+
| `Row row :: rest -> loop (row :: current_rows) acc rest
4443+
| `Spread spread_expr :: rest ->
4444+
let acc =
4445+
match current_rows with
4446+
| [] -> `Spread spread_expr :: acc
4447+
| rows -> `Spread spread_expr :: `Rows (List.rev rows) :: acc
4448+
in
4449+
loop [] acc rest
4450+
in
4451+
loop [] [] parts
4452+
in
43974453
Parser.expect Rbrace p;
4398-
Ast_helper.Exp.apply ~loc
4399-
(Ast_helper.Exp.ident ~loc
4400-
(Location.mkloc
4401-
(Longident.Ldot (Longident.Lident Primitive_modules.dict, "make"))
4402-
loc))
4403-
[(Asttypes.Nolabel, Ast_helper.Exp.array ~loc key_value_pairs)]
4454+
match grouped_parts with
4455+
| [] -> make_dict_chunk ~loc_override:loc []
4456+
| [`Rows rows] -> make_dict_chunk ~loc_override:loc rows
4457+
| `Rows target_rows :: source_parts ->
4458+
make_dict_spread (make_dict_chunk target_rows) source_parts
4459+
| source_parts -> make_dict_spread (make_dict_chunk []) source_parts
44044460

44054461
and parse_array_exp p =
44064462
let start_pos = p.Parser.start_pos in

compiler/syntax/src/res_grammar.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ let is_mod_expr_start = function
224224
| _ -> false
225225

226226
let is_dict_row_start = function
227-
| Token.String _ -> true
227+
| Token.DotDotDot | String _ -> true
228228
| _ -> false
229229

230230
let is_record_row_start = function

compiler/syntax/src/res_parsetree_viewer.ml

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ let has_dict_pattern_attribute attrs =
8686
txt = "res.dictPattern")
8787
|> Option.is_some
8888

89+
let has_dict_spread_attribute attrs =
90+
attrs
91+
|> List.find_opt (fun (({txt}, _) : Parsetree.attribute) ->
92+
txt = "res.dictSpread")
93+
|> Option.is_some
94+
95+
type dict_expr_part =
96+
| DictExprRows of Parsetree.expression
97+
| DictExprSpread of Parsetree.expression
98+
8999
let collect_array_expressions expr =
90100
match expr.pexp_desc with
91101
| Pexp_array exprs -> (exprs, None)
@@ -250,7 +260,7 @@ let filter_parsing_attrs attrs =
250260
Location.txt =
251261
( "res.braces" | "ns.braces" | "res.iflet" | "res.ternary"
252262
| "res.await" | "res.template" | "res.taggedTemplate"
253-
| "res.patVariantSpread" | "res.dictPattern"
263+
| "res.patVariantSpread" | "res.dictPattern" | "res.dictSpread"
254264
| "res.inlineRecordDefinition" );
255265
},
256266
_ ) ->
@@ -586,7 +596,7 @@ let is_printable_attribute attr =
586596
Location.txt =
587597
( "res.iflet" | "res.braces" | "ns.braces" | "JSX" | "res.await"
588598
| "res.template" | "res.taggedTemplate" | "res.ternary"
589-
| "res.inlineRecordDefinition" );
599+
| "res.inlineRecordDefinition" | "res.dictSpread" );
590600
},
591601
_ ) ->
592602
false
@@ -738,6 +748,68 @@ let is_spread_belt_array_concat expr =
738748
has_spread_attr expr.pexp_attributes
739749
| _ -> false
740750

751+
let is_tuple_array (expr : Parsetree.expression) =
752+
let is_plain_tuple (expr : Parsetree.expression) =
753+
match expr with
754+
| {pexp_desc = Pexp_tuple _} -> true
755+
| _ -> false
756+
in
757+
match expr with
758+
| {pexp_desc = Pexp_array items} -> List.for_all is_plain_tuple items
759+
| _ -> false
760+
761+
let collect_spread_dict_expr_parts expr =
762+
let extract_literal_dict_rows (expr : Parsetree.expression) =
763+
match expr with
764+
| {
765+
pexp_desc =
766+
Pexp_apply
767+
{
768+
funct =
769+
{
770+
pexp_desc =
771+
Pexp_ident
772+
{txt = Longident.Ldot (Lident "Primitive_dict", "make")};
773+
};
774+
args = [(Nolabel, key_values)];
775+
};
776+
}
777+
when is_tuple_array key_values ->
778+
Some key_values
779+
| _ -> None
780+
in
781+
let is_empty_tuple_array (expr : Parsetree.expression) =
782+
match expr.pexp_desc with
783+
| Pexp_array [] -> true
784+
| _ -> false
785+
in
786+
match expr with
787+
| {
788+
pexp_desc =
789+
Pexp_apply
790+
{
791+
funct =
792+
{
793+
pexp_desc =
794+
Pexp_ident
795+
{txt = Longident.Ldot (Lident "Primitive_dict", "spread")};
796+
pexp_attributes;
797+
};
798+
args =
799+
[(Nolabel, target_expr); (Nolabel, {pexp_desc = Pexp_array sources})];
800+
};
801+
}
802+
when has_dict_spread_attribute pexp_attributes ->
803+
let to_part expr =
804+
match extract_literal_dict_rows expr with
805+
| Some rows_expr ->
806+
if is_empty_tuple_array rows_expr then None
807+
else Some (DictExprRows rows_expr)
808+
| None -> Some (DictExprSpread expr)
809+
in
810+
Some (List.filter_map to_part (target_expr :: sources))
811+
| _ -> None
812+
741813
(* Blue | Red | Green -> [Blue; Red; Green] *)
742814
let collect_or_pattern_chain pat =
743815
let rec loop pattern chain =
@@ -797,16 +869,6 @@ let is_rewritten_underscore_apply_sugar expr =
797869
| Pexp_ident {txt = Longident.Lident "_"} -> true
798870
| _ -> false
799871

800-
let is_tuple_array (expr : Parsetree.expression) =
801-
let is_plain_tuple (expr : Parsetree.expression) =
802-
match expr with
803-
| {pexp_desc = Pexp_tuple _} -> true
804-
| _ -> false
805-
in
806-
match expr with
807-
| {pexp_desc = Pexp_array items} -> List.for_all is_plain_tuple items
808-
| _ -> false
809-
810872
let get_jsx_prop_loc = function
811873
| Parsetree.JSXPropPunning (_, name) -> name.loc
812874
| Parsetree.JSXPropValue (name, _, value) ->

compiler/syntax/src/res_parsetree_viewer.mli

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ val has_await_attribute : Parsetree.attributes -> bool
1717
val has_inline_record_definition_attribute : Parsetree.attributes -> bool
1818
val has_res_pat_variant_spread_attribute : Parsetree.attributes -> bool
1919
val has_dict_pattern_attribute : Parsetree.attributes -> bool
20+
val has_dict_spread_attribute : Parsetree.attributes -> bool
21+
22+
type dict_expr_part =
23+
| DictExprRows of Parsetree.expression
24+
| DictExprSpread of Parsetree.expression
2025

2126
type if_condition_kind =
2227
| If of Parsetree.expression
@@ -132,6 +137,9 @@ val is_spread_belt_list_concat : Parsetree.expression -> bool
132137

133138
val is_spread_belt_array_concat : Parsetree.expression -> bool
134139

140+
val collect_spread_dict_expr_parts :
141+
Parsetree.expression -> dict_expr_part list option
142+
135143
val collect_or_pattern_chain : Parsetree.pattern -> Parsetree.pattern list
136144

137145
val process_braces_attr :

0 commit comments

Comments
 (0)