Skip to content

Commit 0d0d7a0

Browse files
authored
Array spread syntax support (#6608)
* Implement parser to get spread expressions * Flatten array and call concatMany just once * Remove empty array in the parser output * Implement spread array printer * Update array parsing tests * Clean up * Fix tests * Fix extra whitespace got deleted by editor and run ocamlformat * Update changelog
1 parent 26c2303 commit 0d0d7a0

File tree

10 files changed

+181
-72
lines changed

10 files changed

+181
-72
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
- Experimental support of tagged template literals, e.g. ```sql`select * from ${table}```. https://github.com/rescript-lang/rescript-compiler/pull/6250
3131
- Experimental support for generic/custom JSX transforms. https://github.com/rescript-lang/rescript-compiler/pull/6565
3232
- `dict` is now a builtin type. https://github.com/rescript-lang/rescript-compiler/pull/6590
33+
- Add support for array spread. https://github.com/rescript-lang/rescript-compiler/pull/6608
3334

3435
#### :bug: Bug Fix
3536

jscomp/syntax/src/res_core.ml

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,6 @@ module ErrorMessages = struct
7878
+ Array size check + `get` checks on the current pattern. If it's to \
7979
obtain a subarray, use `Array.sub` or `Belt.Array.slice`."
8080

81-
let arrayExprSpread =
82-
"Arrays can't use the `...` spread currently. Please use `concat` or other \
83-
Array helpers."
84-
8581
let recordExprSpread =
8682
"Records can only have one `...` spread, at the beginning.\n\
8783
Explanation: since records have a known, fixed shape, a spread like `{a, \
@@ -3920,36 +3916,60 @@ and parseListExpr ~startPos p =
39203916
loc))
39213917
[(Asttypes.Nolabel, Ast_helper.Exp.array ~loc listExprs)]
39223918

3923-
(* Overparse ... and give a nice error message *)
3924-
and parseNonSpreadExp ~msg p =
3925-
let () =
3926-
match p.Parser.token with
3927-
| DotDotDot ->
3928-
Parser.err p (Diagnostics.message msg);
3929-
Parser.next p
3930-
| _ -> ()
3931-
in
3932-
match p.Parser.token with
3933-
| token when Grammar.isExprStart token -> (
3934-
let expr = parseExpr p in
3935-
match p.Parser.token with
3936-
| Colon ->
3937-
Parser.next p;
3938-
let typ = parseTypExpr p in
3939-
let loc = mkLoc expr.pexp_loc.loc_start typ.ptyp_loc.loc_end in
3940-
Some (Ast_helper.Exp.constraint_ ~loc expr typ)
3941-
| _ -> Some expr)
3942-
| _ -> None
3943-
39443919
and parseArrayExp p =
39453920
let startPos = p.Parser.startPos in
39463921
Parser.expect Lbracket p;
3947-
let exprs =
3948-
parseCommaDelimitedRegion p ~grammar:Grammar.ExprList ~closing:Rbracket
3949-
~f:(parseNonSpreadExp ~msg:ErrorMessages.arrayExprSpread)
3922+
let split_by_spread exprs =
3923+
List.fold_left
3924+
(fun acc curr ->
3925+
match (curr, acc) with
3926+
| (true, expr, startPos, endPos), _ ->
3927+
(* find a spread expression, prepend a new sublist *)
3928+
([], Some expr, startPos, endPos) :: acc
3929+
| ( (false, expr, startPos, _endPos),
3930+
(no_spreads, spread, _accStartPos, accEndPos) :: acc ) ->
3931+
(* find a non-spread expression, and the accumulated is not empty,
3932+
* prepend to the first sublist, and update the loc of the first sublist *)
3933+
(expr :: no_spreads, spread, startPos, accEndPos) :: acc
3934+
| (false, expr, startPos, endPos), [] ->
3935+
(* find a non-spread expression, and the accumulated is empty *)
3936+
[([expr], None, startPos, endPos)])
3937+
[] exprs
3938+
in
3939+
let listExprsRev =
3940+
parseCommaDelimitedReversedList p ~grammar:Grammar.ExprList
3941+
~closing:Rbracket ~f:parseSpreadExprRegionWithLoc
39503942
in
39513943
Parser.expect Rbracket p;
3952-
Ast_helper.Exp.array ~loc:(mkLoc startPos p.prevEndPos) exprs
3944+
let loc = mkLoc startPos p.prevEndPos in
3945+
let collectExprs = function
3946+
| [], Some spread, _startPos, _endPos -> [spread]
3947+
| exprs, Some spread, _startPos, _endPos ->
3948+
let els = Ast_helper.Exp.array ~loc exprs in
3949+
[els; spread]
3950+
| exprs, None, _startPos, _endPos ->
3951+
let els = Ast_helper.Exp.array ~loc exprs in
3952+
[els]
3953+
in
3954+
match split_by_spread listExprsRev with
3955+
| [] -> Ast_helper.Exp.array ~loc:(mkLoc startPos p.prevEndPos) []
3956+
| [(exprs, None, _, _)] ->
3957+
Ast_helper.Exp.array ~loc:(mkLoc startPos p.prevEndPos) exprs
3958+
| exprs ->
3959+
let xs = List.map collectExprs exprs in
3960+
let listExprs =
3961+
List.fold_right
3962+
(fun exprs1 acc ->
3963+
List.fold_right (fun expr1 acc1 -> expr1 :: acc1) exprs1 acc)
3964+
xs []
3965+
in
3966+
Ast_helper.Exp.apply ~loc
3967+
(Ast_helper.Exp.ident ~loc ~attrs:[spreadAttr]
3968+
(Location.mkloc
3969+
(Longident.Ldot
3970+
(Longident.Ldot (Longident.Lident "Belt", "Array"), "concatMany"))
3971+
loc))
3972+
[(Asttypes.Nolabel, Ast_helper.Exp.array ~loc listExprs)]
39533973

39543974
(* TODO: check attributes in the case of poly type vars,
39553975
* might be context dependend: parseFieldDeclaration (see ocaml) *)

jscomp/syntax/src/res_parsetree_viewer.ml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ let hasAwaitAttribute attrs =
104104
| _ -> false)
105105
attrs
106106

107+
let collectArrayExpressions expr =
108+
match expr.pexp_desc with
109+
| Pexp_array exprs -> (exprs, None)
110+
| _ -> ([], Some expr)
111+
107112
let collectListExpressions expr =
108113
let rec collect acc expr =
109114
match expr.pexp_desc with
@@ -678,6 +683,17 @@ let isSpreadBeltListConcat expr =
678683
hasSpreadAttr expr.pexp_attributes
679684
| _ -> false
680685

686+
let isSpreadBeltArrayConcat expr =
687+
match expr.pexp_desc with
688+
| Pexp_ident
689+
{
690+
txt =
691+
Longident.Ldot
692+
(Longident.Ldot (Longident.Lident "Belt", "Array"), "concatMany");
693+
} ->
694+
hasSpreadAttr expr.pexp_attributes
695+
| _ -> false
696+
681697
(* Blue | Red | Green -> [Blue; Red; Green] *)
682698
let collectOrPatternChain pat =
683699
let rec loop pattern chain =

jscomp/syntax/src/res_parsetree_viewer.mli

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ val collectIfExpressions :
4646
(Location.t * ifConditionKind * Parsetree.expression) list
4747
* Parsetree.expression option
4848

49+
val collectArrayExpressions :
50+
Parsetree.expression ->
51+
Parsetree.expression list * Parsetree.expression option
52+
4953
val collectListExpressions :
5054
Parsetree.expression ->
5155
Parsetree.expression list * Parsetree.expression option
@@ -142,6 +146,8 @@ val hasTemplateLiteralAttr : Parsetree.attributes -> bool
142146

143147
val isSpreadBeltListConcat : Parsetree.expression -> bool
144148

149+
val isSpreadBeltArrayConcat : Parsetree.expression -> bool
150+
145151
val collectOrPatternChain : Parsetree.pattern -> Parsetree.pattern list
146152

147153
val processBracesAttr :

jscomp/syntax/src/res_printer.ml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3046,6 +3046,9 @@ and printExpression ~state (e : Parsetree.expression) cmtTbl =
30463046
Doc.rbrace;
30473047
])
30483048
| extension -> printExtension ~state ~atModuleLvl:false extension cmtTbl)
3049+
| Pexp_apply (e, [(Nolabel, {pexp_desc = Pexp_array subLists})])
3050+
when ParsetreeViewer.isSpreadBeltArrayConcat e ->
3051+
printBeltArrayConcatApply ~state subLists cmtTbl
30493052
| Pexp_apply (e, [(Nolabel, {pexp_desc = Pexp_array subLists})])
30503053
when ParsetreeViewer.isSpreadBeltListConcat e ->
30513054
printBeltListConcatApply ~state subLists cmtTbl
@@ -3813,6 +3816,61 @@ and printBinaryExpression ~state (expr : Parsetree.expression) cmtTbl =
38133816
])
38143817
| _ -> Doc.nil
38153818

3819+
and printBeltArrayConcatApply ~state subLists cmtTbl =
3820+
let makeSpreadDoc commaBeforeSpread = function
3821+
| Some expr ->
3822+
Doc.concat
3823+
[
3824+
commaBeforeSpread;
3825+
Doc.dotdotdot;
3826+
(let doc = printExpressionWithComments ~state expr cmtTbl in
3827+
match Parens.expr expr with
3828+
| Parens.Parenthesized -> addParens doc
3829+
| Braced braces -> printBraces doc expr braces
3830+
| Nothing -> doc);
3831+
]
3832+
| None -> Doc.nil
3833+
in
3834+
let makeSubListDoc (expressions, spread) =
3835+
let commaBeforeSpread =
3836+
match expressions with
3837+
| [] -> Doc.nil
3838+
| _ -> Doc.concat [Doc.text ","; Doc.line]
3839+
in
3840+
let spreadDoc = makeSpreadDoc commaBeforeSpread spread in
3841+
Doc.concat
3842+
[
3843+
Doc.join
3844+
~sep:(Doc.concat [Doc.text ","; Doc.line])
3845+
(List.map
3846+
(fun expr ->
3847+
let doc = printExpressionWithComments ~state expr cmtTbl in
3848+
match Parens.expr expr with
3849+
| Parens.Parenthesized -> addParens doc
3850+
| Braced braces -> printBraces doc expr braces
3851+
| Nothing -> doc)
3852+
expressions);
3853+
spreadDoc;
3854+
]
3855+
in
3856+
Doc.group
3857+
(Doc.concat
3858+
[
3859+
Doc.lbracket;
3860+
Doc.indent
3861+
(Doc.concat
3862+
[
3863+
Doc.softLine;
3864+
Doc.join
3865+
~sep:(Doc.concat [Doc.text ","; Doc.line])
3866+
(List.map makeSubListDoc
3867+
(List.map ParsetreeViewer.collectArrayExpressions subLists));
3868+
]);
3869+
Doc.trailingComma;
3870+
Doc.softLine;
3871+
Doc.rbracket;
3872+
])
3873+
38163874
and printBeltListConcatApply ~state subLists cmtTbl =
38173875
let makeSpreadDoc commaBeforeSpread = function
38183876
| Some expr ->

jscomp/syntax/tests/parsing/errors/other/expected/spread.res.txt

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,67 @@
11

22
Syntax error!
3-
tests/parsing/errors/other/spread.res:1:12-14
3+
tests/parsing/errors/other/spread.res:1:6-8
44

5-
1 │ let arr = [...x, ...y]
6-
2 │ let [...arr, _] = [1, 2, 3]
7-
3 │
8-
9-
Arrays can't use the `...` spread currently. Please use `concat` or other Array helpers.
10-
11-
12-
Syntax error!
13-
tests/parsing/errors/other/spread.res:2:6-8
14-
15-
1 │ let arr = [...x, ...y]
16-
2 │ let [...arr, _] = [1, 2, 3]
17-
3 │
18-
4 │ let record = {...x, ...y}
5+
1 │ let [...arr, _] = [1, 2, 3]
6+
2 │
7+
3 │ let record = {...x, ...y}
198

209
Array's `...` spread is not supported in pattern matches.
2110
Explanation: such spread would create a subarray; out of performance concern, our pattern matching currently guarantees to never create new intermediate data.
2211
Solution: if it's to validate the first few elements, use a `when` clause + Array size check + `get` checks on the current pattern. If it's to obtain a subarray, use `Array.sub` or `Belt.Array.slice`.
2312

2413

2514
Syntax error!
26-
tests/parsing/errors/other/spread.res:4:21-23
15+
tests/parsing/errors/other/spread.res:3:21-23
2716

28-
2 │ let [...arr, _] = [1, 2, 3]
29-
3
30-
4 │ let record = {...x, ...y}
31-
5 │ let {...x, ...y} = myRecord
32-
6
17+
1 │ let [...arr, _] = [1, 2, 3]
18+
2
19+
3 │ let record = {...x, ...y}
20+
4 │ let {...x, ...y} = myRecord
21+
5
3322

3423
Records can only have one `...` spread, at the beginning.
3524
Explanation: since records have a known, fixed shape, a spread like `{a, ...b}` wouldn't make sense, as `b` would override every field of `a` anyway.
3625

3726

3827
Syntax error!
39-
tests/parsing/errors/other/spread.res:5:15-18
28+
tests/parsing/errors/other/spread.res:4:15-18
4029

41-
3
42-
4 │ let record = {...x, ...y}
43-
5 │ let {...x, ...y} = myRecord
44-
6
45-
7 │ let list{...x, ...y} = myList
30+
2
31+
3 │ let record = {...x, ...y}
32+
4 │ let {...x, ...y} = myRecord
33+
5
34+
6 │ let list{...x, ...y} = myList
4635

4736
Record's `...` spread is not supported in pattern matches.
4837
Explanation: you can't collect a subset of a record's field into its own record, since a record needs an explicit declaration and that subset wouldn't have one.
4938
Solution: you need to pull out each field you want explicitly.
5039

5140

5241
Syntax error!
53-
tests/parsing/errors/other/spread.res:7:13-22
42+
tests/parsing/errors/other/spread.res:6:13-22
5443

55-
5 │ let {...x, ...y} = myRecord
56-
6
57-
7 │ let list{...x, ...y} = myList
58-
8
59-
9 │ type t = {...a}
44+
4 │ let {...x, ...y} = myRecord
45+
5
46+
6 │ let list{...x, ...y} = myList
47+
7
48+
8 │ type t = {...a}
6049

6150
List pattern matches only supports one `...` spread, at the end.
6251
Explanation: a list spread at the tail is efficient, but a spread in the middle would create new lists; out of performance concern, our pattern matching currently guarantees to never create new intermediate data.
6352

6453

6554
Syntax error!
66-
tests/parsing/errors/other/spread.res:10:20
55+
tests/parsing/errors/other/spread.res:9:20
6756

68-
8
69-
9 │ type t = {...a}
70-
10 │ type t = Foo({...a})
71-
11 │ type t = option<foo, {...x}>
72-
12
57+
7
58+
8 │ type t = {...a}
59+
9 │ type t = Foo({...a})
60+
10 │ type t = option<foo, {...x}>
61+
11
7362

7463
I'm not sure what to parse here when looking at ")".
7564

76-
let arr = [|x;y|]
7765
let [|arr;_|] = [|1;2;3|]
7866
let record = { x with y }
7967
let { x; y } = myRecord

jscomp/syntax/tests/parsing/errors/other/spread.res

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
let arr = [...x, ...y]
21
let [...arr, _] = [1, 2, 3]
32

43
let record = {...x, ...y}

jscomp/syntax/tests/parsing/grammar/expressions/array.res

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,18 @@ let x = [1, 2, 3,]
55

66
// with constrained expressions
77
let x = [1 :int, (2: int), 3 : int]
8+
9+
// spread
10+
let x = [4, 5, ...y]
11+
12+
// spread anywhere
13+
let x = [4, 5, ...y, 7, ...y]
14+
15+
// spread constrained expressions
16+
let x = [4, 5, ...y: array<int>]
17+
18+
// spread with other variable
19+
let x = [4, 5, k, ...y]
20+
21+
// the only spread
22+
let x = [...y]
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
let x = [|1;2;3|]
22
let x = [|1;2;3|]
3-
let x = [|(1 : int);(2 : int);(3 : int)|]
3+
let x = [|(1 : int);(2 : int);(3 : int)|]
4+
let x = ((Belt.Array.concatMany)[@res.spread ]) [|[|4;5|];y|]
5+
let x = ((Belt.Array.concatMany)[@res.spread ]) [|[|4;5|];y;[|7|];y|]
6+
let x = ((Belt.Array.concatMany)[@res.spread ]) [|[|4;5|];(y : int array)|]
7+
let x = ((Belt.Array.concatMany)[@res.spread ]) [|[|4;5;k|];y|]
8+
let x = ((Belt.Array.concatMany)[@res.spread ]) [|y|]

jscomp/syntax/tests/parsing/recovery/expression/expected/list.res.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ let flags =
3333
let rec loop items =
3434
((match items with
3535
| [|{js|-pp|js};_ppFlag;rest|] -> loop rest
36-
| [|x;rest|] -> [|x;(loop rest)|]
36+
| [|x;rest|] ->
37+
((Belt.Array.concatMany)[@res.spread ]) [|[|x|];(loop rest)|]
3738
| [||] -> [||])
3839
[@res.braces ]) in
3940
(loop parts) |> (String.concat {js| |js}))

0 commit comments

Comments
 (0)